diff --git a/.citools/src/cog/go.mod b/.citools/src/cog/go.mod index 83bc319bb5e..6a2eab53809 100644 --- a/.citools/src/cog/go.mod +++ b/.citools/src/cog/go.mod @@ -10,7 +10,7 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/proto v1.13.2 // indirect - github.com/expr-lang/expr v1.17.0 // indirect + github.com/expr-lang/expr v1.17.7 // indirect github.com/getkin/kin-openapi v0.132.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2c78e72e3d9..03f6fa8697e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,7 +24,6 @@ /NOTICE.md @torkelo /README.md @grafana/docs-grafana /ROADMAP.md @torkelo -/SECURITY.md @grafana/security-team /SUPPORT.md @torkelo /WORKFLOW.md @torkelo /contribute/ @grafana/grafana-community-support @@ -85,6 +84,7 @@ # Git Sync frontend owned by frontend team as a whole. /apps/alerting/ @grafana/alerting-backend +/apps/quotas/ @grafana/grafana-search-and-storage /apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad /apps/folder/ @grafana/grafana-app-platform-squad /apps/playlist/ @grafana/grafana-app-platform-squad @@ -207,7 +207,7 @@ /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 +/pkg/tsdb/opentsdb/ @grafana/oss-big-tent /pkg/util/ @grafana/grafana-backend-group /pkg/web/ @grafana/grafana-backend-group @@ -259,7 +259,7 @@ /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-opentsdb/ @grafana/oss-big-tent /devenv/dev-dashboards/datasource-influxdb/ @grafana/partner-datasources /devenv/dev-dashboards/datasource-mssql/ @grafana/partner-datasources /devenv/dev-dashboards/datasource-loki/ @grafana/plugins-platform-frontend @@ -306,7 +306,7 @@ /devenv/docker/blocks/mysql_exporter/ @grafana/oss-big-tent /devenv/docker/blocks/mysql_opendata/ @grafana/oss-big-tent /devenv/docker/blocks/mysql_tests/ @grafana/oss-big-tent -/devenv/docker/blocks/opentsdb/ @grafana/partner-datasources +/devenv/docker/blocks/opentsdb/ @grafana/oss-big-tent /devenv/docker/blocks/postgres/ @grafana/oss-big-tent /devenv/docker/blocks/postgres_tests/ @grafana/oss-big-tent /devenv/docker/blocks/prometheus/ @grafana/oss-big-tent @@ -425,6 +425,7 @@ 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/alerting-suite/ @grafana/alerting-frontend /e2e-playwright/cloud-plugins-suite/ @grafana/partner-datasources /e2e-playwright/dashboard-new-layouts/ @grafana/dashboards-squad /e2e-playwright/dashboard-cujs/ @grafana/dashboards-squad @@ -500,7 +501,6 @@ i18next.config.ts @grafana/grafana-frontend-platform /e2e-playwright/various-suite/filter-annotations.spec.ts @grafana/dashboards-squad /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 @@ -519,7 +519,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /e2e-playwright/various-suite/solo-route.spec.ts @grafana/dashboards-squad /e2e-playwright/various-suite/trace-view-scrolling.spec.ts @grafana/observability-traces-and-profiling /e2e-playwright/various-suite/verify-i18n.spec.ts @grafana/grafana-frontend-platform -/e2e-playwright/various-suite/visualization-suggestions.spec.ts @grafana/dashboards-squad +/e2e-playwright/various-suite/visualization-suggestions*.spec.ts @grafana/dataviz-squad /e2e-playwright/various-suite/perf-test.spec.ts @grafana/grafana-frontend-platform # Packages @@ -959,6 +959,7 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/features/notifications/ @grafana/grafana-search-navigate-organise /public/app/features/org/ @grafana/grafana-search-navigate-organise /public/app/features/panel/ @grafana/dashboards-squad +/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @grafana/dataviz-squad /public/app/features/panel/suggestions/ @grafana/dataviz-squad /public/app/features/playlist/ @grafana/dashboards-squad /public/app/features/plugins/ @grafana/plugins-platform-frontend @@ -1103,7 +1104,7 @@ eslint-suppressions.json @grafanabot /public/app/plugins/datasource/mixed/ @grafana/dashboards-squad /public/app/plugins/datasource/mssql/ @grafana/partner-datasources /public/app/plugins/datasource/mysql/ @grafana/oss-big-tent -/public/app/plugins/datasource/opentsdb/ @grafana/partner-datasources +/public/app/plugins/datasource/opentsdb/ @grafana/oss-big-tent /public/app/plugins/datasource/grafana-postgresql-datasource/ @grafana/oss-big-tent /public/app/plugins/datasource/prometheus/ @grafana/oss-big-tent /public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-datasources diff --git a/.github/actions/build-package/action.yml b/.github/actions/build-package/action.yml index dd5aaa13650..978c6645240 100644 --- a/.github/actions/build-package/action.yml +++ b/.github/actions/build-package/action.yml @@ -82,14 +82,6 @@ inputs: description: Docker registry of produced images default: docker.io required: false - ubuntu-base: - type: string - default: 'ubuntu:22.04' - required: false - alpine-base: - type: string - default: 'alpine:3.22' - required: false outputs: dist-dir: description: Directory where artifacts are placed @@ -134,13 +126,11 @@ runs: UBUNTU_TAG_FORMAT: ${{ inputs.docker-tag-format-ubuntu }} CHECKSUM: ${{ inputs.checksum }} VERIFY: ${{ inputs.verify }} - ALPINE_BASE: ${{ inputs.alpine-base }} - UBUNTU_BASE: ${{ inputs.ubuntu-base }} with: verb: run dagger-flags: --verbose=0 version: 0.18.8 - args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --alpine-base=${ALPINE_BASE} --ubuntu-base=${UBUNTU_BASE} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE + args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE - id: output shell: bash env: diff --git a/.github/actions/change-detection/action.yml b/.github/actions/change-detection/action.yml index 2c6b46606f4..2b8484d7cf6 100644 --- a/.github/actions/change-detection/action.yml +++ b/.github/actions/change-detection/action.yml @@ -95,9 +95,11 @@ runs: - 'nx.json' - 'tsconfig.json' - '.yarn/**' + - 'apps/dashboard/pkg/migration/**' - '${{ inputs.self }}' e2e: - 'e2e/**' + - 'e2e-playwright/**' - '.github/actions/setup-enterprise/**' - '.github/actions/checkout/**' - 'emails/**' diff --git a/.github/commands.json b/.github/commands.json index 29106576726..0db8a607b53 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -1226,5 +1226,13 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/69" } + }, + { + "type": "label", + "name": "area/suggestions", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/56" + } } ] diff --git a/.github/pr-commands.json b/.github/pr-commands.json index 8b4472fe02a..bfff4753773 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -365,7 +365,9 @@ "type": "changedfiles", "matches": [ "public/app/plugins/panel/gauge/**/*", - "/packages/grafana-ui/src/components/Gauge/**/*" + "public/app/plugins/panel/radialbar/**/*", + "/packages/grafana-ui/src/components/Gauge/**/*", + "/packages/grafana-ui/src/components/RadialGauge/**/*" ], "action": "updateLabel", "addLabel": "area/panel/gauge" @@ -469,5 +471,15 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/190" } + }, + { + "type": "changedfiles", + "matches": [ + "public/app/features/panel/suggestions/**/*", + "public/app/plugins/panel/**/suggestions.ts", + "packages/grafana-data/src/types/suggestions*" + ], + "action": "updateLabel", + "addLabel": "area/suggestions" } ] diff --git a/.github/workflows/auto-triager/labels.txt b/.github/workflows/auto-triager/labels.txt index 18ef2df93d8..451719ee0f7 100644 --- a/.github/workflows/auto-triager/labels.txt +++ b/.github/workflows/auto-triager/labels.txt @@ -85,6 +85,7 @@ area/scenes area/search area/security area/streaming +area/suggestions area/templating/repeating area/tooltip area/transformations diff --git a/.github/workflows/cleanup-branches.yml b/.github/workflows/cleanup-branches.yml index 7f4a6909e6e..7f359309cca 100644 --- a/.github/workflows/cleanup-branches.yml +++ b/.github/workflows/cleanup-branches.yml @@ -14,5 +14,5 @@ jobs: - uses: actions/checkout@v5 - uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v0.2.1 with: - dry-run: true + dry-run: false max-date: "1 month ago" diff --git a/.github/workflows/ephemeral-instances-pr-comment.yml b/.github/workflows/ephemeral-instances-pr-comment.yml index d78b5b8f599..c116e43a443 100644 --- a/.github/workflows/ephemeral-instances-pr-comment.yml +++ b/.github/workflows/ephemeral-instances-pr-comment.yml @@ -33,6 +33,16 @@ jobs: GCOM_TOKEN=ephemeral-instances-bot:gcom-token REGISTRY=ephemeral-instances-bot:registry GCP_SA_ACCOUNT_KEY_BASE64=ephemeral-instances-bot:sa-key + # Secrets placed in the ci/common/ path in Vault + common_secrets: | + DOCKERHUB_USERNAME=dockerhub:username + DOCKERHUB_PASSWORD=dockerhub:password + + - name: Log in to Docker Hub to avoid unauthenticated image pull rate-limiting + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_PASSWORD }} - name: Generate a GitHub app installation token id: generate_token diff --git a/.github/workflows/pr-patch-check-event.yml b/.github/workflows/pr-patch-check-event.yml index 2b30e0fa375..86a4ad64917 100644 --- a/.github/workflows/pr-patch-check-event.yml +++ b/.github/workflows/pr-patch-check-event.yml @@ -12,6 +12,7 @@ on: permissions: id-token: write contents: read + statuses: write # Since this is run on a pull request, we want to apply the patches intended for the # target branch onto the source branch, to verify compatibility before merging. diff --git a/.github/workflows/pr-patch-check.yml b/.github/workflows/pr-patch-check.yml index 52f75a05ff2..8a1f70174d7 100644 --- a/.github/workflows/pr-patch-check.yml +++ b/.github/workflows/pr-patch-check.yml @@ -29,6 +29,10 @@ permissions: # target branch onto the source branch, to verify compatibility before merging. jobs: dispatch-job: + # If the source is not from a fork then dispatch the job to the workflow. + # This will fail on forks when trying to broker a token, so instead, forks will create the required status and mark + # it as a success + if: ${{ ! github.event.pull_request.head.repo.fork }} env: HEAD_REF: ${{ inputs.head_ref }} BASE_REF: ${{ github.base_ref }} @@ -76,3 +80,20 @@ jobs: triggering_github_handle: SENDER } }) + dispatch-job-fork: + # If the source is from a fork then use the built-in workflow token to create the same status and unconditionally + # mark it as a success. + if: ${{ github.event.pull_request.head.repo.fork }} + permissions: + statuses: write + runs-on: ubuntu-latest + steps: + - name: Create status + uses: myrotvorets/set-commit-status-action@6d6905c99cd24a4a2cbccc720b62dc6ca5587141 + with: + token: ${{ github.token }} + sha: ${{ inputs.pr_commit_sha }} + repo: ${{ inputs.repo }} + status: success + context: "Test Patches (event)" + description: "Test Patches (event) on a fork" diff --git a/.github/workflows/release-comms.yml b/.github/workflows/release-comms.yml index 58ebe84dbc0..fb65b2a3eed 100644 --- a/.github/workflows/release-comms.yml +++ b/.github/workflows/release-comms.yml @@ -111,12 +111,13 @@ jobs: ownerRepo: 'grafana/grafana-enterprise' from: ${{ needs.setup.outputs.release_branch }} to: ${{ needs.create_next_release_branch_enterprise.outputs.branch }} - post_changelog_on_forum: - needs: setup - uses: grafana/grafana/.github/workflows/community-release.yml@main - with: - version: ${{ needs.setup.outputs.version }} - dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} + # Removed this for now since it doesn't work + # post_changelog_on_forum: + # needs: setup + # uses: grafana/grafana/.github/workflows/community-release.yml@main + # with: + # version: ${{ needs.setup.outputs.version }} + # dry_run: ${{ needs.setup.outputs.dry_run == 'true' }} create_github_release: # a github release requires a git tag # The github-release action retrieves the changelog using the /repos/grafana/grafana/contents/CHANGELOG.md API diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 999ccbc5e4a..9c332d0675a 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -90,6 +90,7 @@ jobs: with: persist-credentials: false ref: ${{ inputs.grafana_commit }} + fetch-depth: 2 # Need HEAD~1 for e2e-selectors change detection - name: Setup Node uses: ./.github/actions/setup-node @@ -127,3 +128,43 @@ jobs: env: NPM_TAG: ${{ steps.npm-tag.outputs.NPM_TAG }} run: ./scripts/publish-npm-packages.sh --dist-tag "$NPM_TAG" --registry 'https://registry.npmjs.org/' + + # Notify plugin-tools when e2e-selectors changes so it can update its bundled version + - name: Check for e2e-selectors changes + id: check-e2e-changes + run: | + CHANGES=$(git diff --name-only HEAD~1 HEAD -- packages/grafana-e2e-selectors | wc -l) + echo "changes=$CHANGES" >> "$GITHUB_OUTPUT" + if [ "$CHANGES" -gt 0 ]; then + echo "Detected $CHANGES file(s) changed in packages/grafana-e2e-selectors" + fi + + - name: Get Vault secrets + if: steps.check-e2e-changes.outputs.changes > 0 + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + repo_secrets: | + GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY + + - name: Generate token for plugin-tools + if: steps.check-e2e-changes.outputs.changes > 0 + id: generate_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a + with: + app_id: ${{ vars.DELIVERY_BOT_APP_ID }} + private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }} + repositories: '["plugin-tools"]' + permissions: '{"actions": "write"}' + + - name: Dispatch to plugin-tools + if: steps.check-e2e-changes.outputs.changes > 0 + env: + VERSION: ${{ inputs.version }} + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + echo "Dispatching bump-e2e-selectors workflow to grafana/plugin-tools with version $VERSION" + gh workflow run bump-e2e-selectors.yml \ + --repo grafana/plugin-tools \ + --ref main \ + --field version="$VERSION" diff --git a/.golangci.yml b/.golangci.yml index 3b7871662e2..b52a986d435 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,7 @@ # Others can set up the YAML LSP manually, which supports schemas: https://github.com/redhat-developer/yaml-language-server # $schema: https://golangci-lint.run/jsonschema/golangci.jsonschema.json -version: "2" +version: '2' run: timeout: 15m concurrency: 10 @@ -83,6 +83,16 @@ linters: deny: - pkg: github.com/grafana/grafana/pkg desc: apps/playlist is not allowed to import grafana core + apps-dashboard: + list-mode: lax + files: + - ./apps/dashboard/* + - ./apps/dashboard/**/* + allow: + - github.com/grafana/grafana/pkg/apimachinery + deny: + - pkg: github.com/grafana/grafana/pkg + desc: apps/dashboard is not allowed to import grafana core apps-secret: list-mode: lax files: @@ -281,16 +291,16 @@ linters: text: G306 - linters: - gosec - text: "401" + text: '401' - linters: - gosec - text: "402" + text: '402' - linters: - gosec - text: "501" + text: '501' - linters: - gosec - text: "404" + text: '404' - linters: - errorlint text: non-wrapping format verb for fmt.Errorf diff --git a/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch b/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch new file mode 100644 index 00000000000..730ecce8fb2 --- /dev/null +++ b/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch @@ -0,0 +1,13 @@ +diff --git a/dist/builder-manager/index.js b/dist/builder-manager/index.js +index 3d7f9b213dae1801bda62b31db31b9113e382ccd..212501c63d20146c29db63fb0f6300c6779eecb5 100644 +--- a/dist/builder-manager/index.js ++++ b/dist/builder-manager/index.js +@@ -1970,7 +1970,7 @@ var pa = /^\/($|\?)/, G, C, xt = /* @__PURE__ */ o(async (e) => { + bundle: !0, + minify: !0, + sourcemap: !1, +- conditions: ["browser", "module", "default"], ++ conditions: ["@grafana-app/source", "browser", "module", "default"], + jsxFactory: "React.createElement", + jsxFragment: "React.Fragment", + jsx: "transform", diff --git a/CHANGELOG.md b/CHANGELOG.md index a4dfcb3171a..6bacfafdfd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,101 @@ + + +# 12.3.1 (2025-12-16) + +### Features and enhancements + +- **Alerting:** Update alerting dependency [#114259](https://github.com/grafana/grafana/pull/114259), [@moustafab](https://github.com/moustafab) +- **Azure:** Improved column handling in logs query builder [#114841](https://github.com/grafana/grafana/pull/114841), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Include aggregate columns in logs builder [#114835](https://github.com/grafana/grafana/pull/114835), [@aangelisc](https://github.com/aangelisc) +- **Dependencies:** Bump Go to v1.25.5 [#114751](https://github.com/grafana/grafana/pull/114751), [@macabu](https://github.com/macabu) +- **Docs:** Clarify section title for repeating rows and tabs [#115346](https://github.com/grafana/grafana/pull/115346), [@imatwawana](https://github.com/imatwawana) +- **Plugins:** Add PluginContext to plugins when scenes is disabled [#115064](https://github.com/grafana/grafana/pull/115064), [@hugohaggmark](https://github.com/hugohaggmark) +- **QueryEditorRows:** Clear hideSeriesFrom override on query edit [#114628](https://github.com/grafana/grafana/pull/114628), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) + +### Bug fixes + +- **Azure:** Fix `dcount` aggregation [#114907](https://github.com/grafana/grafana/pull/114907), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix `percentile` syntax [#114707](https://github.com/grafana/grafana/pull/114707), [@aangelisc](https://github.com/aangelisc) +- **Dashboards:** Fix empty space under time controls when a dashboard has a lot of variables [#114730](https://github.com/grafana/grafana/pull/114730), [@oscarkilhed](https://github.com/oscarkilhed) +- **Plugins:** Datasource breadcrumb link should link to settings tab [#113910](https://github.com/grafana/grafana/pull/113910), [@wbrowne](https://github.com/wbrowne) +- **Postgresql:** Fix variable interpolation logic when the variable has multiple values [#114876](https://github.com/grafana/grafana/pull/114876), [@itsmylife](https://github.com/itsmylife) + + + + +# 12.2.3 (2025-12-16) + +### Features and enhancements + +- **Alerting:** Update alerting dependency [#114256](https://github.com/grafana/grafana/pull/114256), [@moustafab](https://github.com/moustafab) +- **Azure:** Improved column handling in logs query builder [#114840](https://github.com/grafana/grafana/pull/114840), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Include aggregate columns in logs builder [#114834](https://github.com/grafana/grafana/pull/114834), [@aangelisc](https://github.com/aangelisc) +- **Dependencies:** Bump Go to v1.25.5 [#114753](https://github.com/grafana/grafana/pull/114753), [@macabu](https://github.com/macabu) +- **Plugins:** Add PluginContext to plugins when scenes is disabled [#115063](https://github.com/grafana/grafana/pull/115063), [@hugohaggmark](https://github.com/hugohaggmark) +- **QueryEditorRows:** Clear hideSeriesFrom override on query edit [#114629](https://github.com/grafana/grafana/pull/114629), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) + +### Bug fixes + +- **Alerting:** Fix contact points issue [#115412](https://github.com/grafana/grafana/pull/115412), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Azure:** Fix `dcount` aggregation [#114906](https://github.com/grafana/grafana/pull/114906), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix `percentile` syntax [#114706](https://github.com/grafana/grafana/pull/114706), [@aangelisc](https://github.com/aangelisc) +- **Postgresql:** Fix variable interpolation logic when the variable has multiple values [#114875](https://github.com/grafana/grafana/pull/114875), [@itsmylife](https://github.com/itsmylife) + + + + +# 12.1.5 (2025-12-16) + +### Features and enhancements + +- **Alerting:** Update alerting dependency [#114254](https://github.com/grafana/grafana/pull/114254), [@moustafab](https://github.com/moustafab) +- **Dependencies:** Bump Go to v1.25.5 [#114755](https://github.com/grafana/grafana/pull/114755), [@macabu](https://github.com/macabu) +- **Docs:** Clarify section title for repeating rows and tabs [#115344](https://github.com/grafana/grafana/pull/115344), [@imatwawana](https://github.com/imatwawana) +- **Plugins:** Add PluginContext to plugins when scenes is disabled [#115062](https://github.com/grafana/grafana/pull/115062), [@hugohaggmark](https://github.com/hugohaggmark) + +### Bug fixes + +- **Alerting:** Fix contact points issue [#115411](https://github.com/grafana/grafana/pull/115411), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Azure:** Fix `dcount` aggregation [#114905](https://github.com/grafana/grafana/pull/114905), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix `percentile` syntax [#114705](https://github.com/grafana/grafana/pull/114705), [@aangelisc](https://github.com/aangelisc) +- **Postgresql:** Fix variable interpolation logic when the variable has multiple values [#114874](https://github.com/grafana/grafana/pull/114874), [@itsmylife](https://github.com/itsmylife) + + + + +# 12.0.8 (2025-12-16) + +### Features and enhancements + +- **Alerting:** Update alerting dependency [#114252](https://github.com/grafana/grafana/pull/114252), [@moustafab](https://github.com/moustafab) +- **Dependencies:** Bump Go to v1.25.5 [#114756](https://github.com/grafana/grafana/pull/114756), [@macabu](https://github.com/macabu) +- **Docs:** Clarify section title for repeating rows and tabs [#115343](https://github.com/grafana/grafana/pull/115343), [@imatwawana](https://github.com/imatwawana) +- **Plugins:** Add PluginContext to plugins when scenes is disabled [#115061](https://github.com/grafana/grafana/pull/115061), [@hugohaggmark](https://github.com/hugohaggmark) + +### Bug fixes + +- **Alerting:** Fix contact points issue [#115410](https://github.com/grafana/grafana/pull/115410), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Azure:** Fix `dcount` aggregation [#114904](https://github.com/grafana/grafana/pull/114904), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix `percentile` syntax [#114704](https://github.com/grafana/grafana/pull/114704), [@aangelisc](https://github.com/aangelisc) +- **Postgresql:** Fix variable interpolation logic when the variable has multiple values [#114873](https://github.com/grafana/grafana/pull/114873), [@itsmylife](https://github.com/itsmylife) + + + + +# 11.6.9 (2025-12-16) + +### Features and enhancements + +- **Alerting:** Update alerting dependency [#114249](https://github.com/grafana/grafana/pull/114249), [@moustafab](https://github.com/moustafab) +- **Dependencies:** Bump Go to v1.25.5 [#114757](https://github.com/grafana/grafana/pull/114757), [@macabu](https://github.com/macabu) +- **PDFTables:** Dynamically shrink font to try and fit whole table in pdf page width (Enterprise) +- **Plugins:** Add PluginContext to plugins when scenes is disabled [#115060](https://github.com/grafana/grafana/pull/115060), [@hugohaggmark](https://github.com/hugohaggmark) + +### Bug fixes + +- **Alerting:** Fix contacts point issues [#115409](https://github.com/grafana/grafana/pull/115409), [@yuri-tceretian](https://github.com/yuri-tceretian) + + # 12.3.0 (2025-11-19) diff --git a/Dockerfile b/Dockerfile index d3f63dd0544..2b16926a836 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ARG JS_SRC=js-builder # Dependabot cannot update dependencies listed in ARGs # By using FROM instructions we can delegate dependency updates to dependabot -FROM alpine:3.22.2 AS alpine-base +FROM alpine:3.23.0 AS alpine-base FROM ubuntu:22.04 AS ubuntu-base FROM golang:1.25.5-alpine AS go-builder-base FROM --platform=${JS_PLATFORM} node:24-alpine AS js-builder-base @@ -93,6 +93,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore COPY pkg/semconv pkg/semconv COPY pkg/aggregator pkg/aggregator COPY apps/playlist apps/playlist +COPY apps/quotas apps/quotas COPY apps/plugins apps/plugins COPY apps/shorturl apps/shorturl COPY apps/annotation apps/annotation diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 42c58f9cd67..00000000000 --- a/SECURITY.md +++ /dev/null @@ -1,29 +0,0 @@ -# Reporting security issues - -If you think you have found a security vulnerability, we have two routes for reporting security issues. - -Important: Whichever route you choose, we ask you to not disclose the vulnerability before it has been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so. - -[Full guidance on reporting a security issue can be found here](https://grafana.com/legal/report-a-security-issue/). - -This product is in scope for our Bug Bounty Program. To submit a vulnerability report, please visit [Grafana Labs Bug Bounty page](https://app.intigriti.com/programs/grafanalabs/grafanaossbbp/detail) and follow the instructions provided. Our security team will review your submission and get back to you as soon as possible. - ---- - -For products and services outside the scope of our bug bounty program, or if you do not wish to receive a bounty, you can report issues directly to us via email at security@grafana.com. This address can be used for all of Grafana Labs’ open source and commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com). - -Please encrypt your message to us; please use our PGP key. The key fingerprint is: - -225E 6A9B BB15 A37E 95EB 6312 C66A 51CC B44C 27E0 - -The key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0x225E6A9BBB15A37E95EB6312C66A51CCB44C27E0&fingerprint=on&op=index). - -Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. - -**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so. - -## Security announcements - -We will post a summary, remediation, and mitigation details for any patch containing security fixes on the Grafana blog. The security announcement blog posts will be tagged with the [security tag](https://grafana.com/tags/security/). - -You can also track security announcements via the [RSS feed](https://grafana.com/tags/security/index.xml). diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 941d8b9bc0f..84a6ca5f010 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,14 +8,15 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/client-go v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) // transitive dependencies that need replaced @@ -43,6 +44,7 @@ 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.25.1 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -55,6 +57,7 @@ require ( 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 @@ -78,14 +81,15 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bluele/gcache v0.0.2 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // 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 @@ -101,6 +105,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // 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.133.0 // indirect @@ -113,22 +118,24 @@ require ( github.com/go-logr/stdr v1.2.2 // 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/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // 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.22.0 // indirect github.com/go-openapi/strfmt v0.24.0 // indirect - github.com/go-openapi/swag v0.23.1 // 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/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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 @@ -144,12 +151,13 @@ require ( github.com/golang-migrate/migrate/v4 v4.7.0 // 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.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.7.0 // indirect - github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect + github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // 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-20250908063411-6b6da59b5cc4 // indirect @@ -157,11 +165,13 @@ require ( github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect + github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect github.com/grafana/otel-profiling-go v0.5.1 // 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.3 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect @@ -176,6 +186,7 @@ require ( github.com/hashicorp/memberlist v0.5.2 // 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 @@ -190,7 +201,7 @@ require ( 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/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -233,10 +244,10 @@ require ( github.com/prometheus/alertmanager v0.28.2 // 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.3 // indirect + github.com/prometheus/common v0.67.4 // 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/prometheus/procfs v0.19.2 // 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 @@ -248,73 +259,83 @@ require ( github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // 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.5 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + go.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.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/instrumentation/net/http/otelhttp v0.64.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 v1.39.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.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/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.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.45.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.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 gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.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.3.8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/component-base v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/kms v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.40.1 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 4695785bbd1..873cbf6de62 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -228,8 +228,8 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y 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/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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 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= @@ -280,8 +280,9 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +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= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= @@ -406,14 +407,16 @@ 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-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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= 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= @@ -422,28 +425,36 @@ github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0T 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -534,8 +545,8 @@ 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -606,8 +617,10 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z 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-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= +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-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= 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-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -618,10 +631,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -749,6 +762,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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= @@ -811,8 +826,8 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU= github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= @@ -979,6 +994,7 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.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= @@ -996,6 +1012,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= @@ -1003,20 +1020,21 @@ 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= @@ -1036,6 +1054,7 @@ 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.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= @@ -1058,6 +1077,8 @@ 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/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= @@ -1067,10 +1088,11 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 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.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= @@ -1096,6 +1118,7 @@ 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -1112,6 +1135,8 @@ github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+Xq github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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= @@ -1122,13 +1147,15 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ 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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= @@ -1139,16 +1166,24 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN 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.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.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= -go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -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/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= 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.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= -go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= +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.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= @@ -1168,19 +1203,19 @@ go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= 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= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= 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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -1191,12 +1226,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9 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/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= 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= @@ -1208,18 +1243,18 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7i 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.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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 v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 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.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= @@ -1255,8 +1290,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -1267,8 +1302,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-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 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= @@ -1294,13 +1329,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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -1349,8 +1385,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -1373,8 +1409,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.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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= @@ -1387,8 +1423,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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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= @@ -1477,14 +1513,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -1494,8 +1530,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -1559,8 +1595,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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -1707,11 +1743,12 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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= @@ -1759,8 +1796,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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -1770,8 +1807,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 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/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -1808,26 +1845,26 @@ 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.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= -k8s.io/kms v0.34.2/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= +k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE= +k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/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= @@ -1857,10 +1894,10 @@ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index 9c1dd3c0f98..114b1fbabce 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -8,18 +8,24 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "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" + advisorapi "github.com/grafana/grafana/apps/advisor/pkg/apis" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "github.com/grafana/grafana/apps/advisor/pkg/app/checkscheduler" "github.com/grafana/grafana/apps/advisor/pkg/app/checktyperegisterer" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/client-go/rest" ) func New(cfg app.Config) (app.App, error) { @@ -188,3 +194,45 @@ func GetKinds() map[schema.GroupVersion][]resource.Kind { }, } } + +func ProvideAppInstaller( + authorizer authorizer.Authorizer, + checkRegistry checkregistry.CheckService, + cfg *setting.Cfg, + orgService org.Service, +) (*AdvisorAppInstaller, error) { + provider := simple.NewAppProvider(advisorapi.LocalManifest(), nil, New) + pluginConfig := cfg.PluginSettings["grafana-advisor-app"] + specificConfig := checkregistry.AdvisorAppConfig{ + CheckRegistry: checkRegistry, + PluginConfig: pluginConfig, + StackID: cfg.StackID, + OrgService: orgService, + } + appCfg := app.Config{ + KubeConfig: rest.Config{}, + ManifestData: *advisorapi.LocalManifest().ManifestData, + SpecificConfig: specificConfig, + } + + defaultInstaller, err := appsdkapiserver.NewDefaultAppInstaller(provider, appCfg, advisorapi.NewGoTypeAssociator()) + if err != nil { + return nil, err + } + + installer := &AdvisorAppInstaller{ + AppInstaller: defaultInstaller, + authorizer: authorizer, + } + + return installer, nil +} + +type AdvisorAppInstaller struct { + appsdkapiserver.AppInstaller + authorizer authorizer.Authorizer +} + +func (a *AdvisorAppInstaller) GetAuthorizer() authorizer.Authorizer { + return a.authorizer +} diff --git a/apps/advisor/pkg/app/authorizer.go b/apps/advisor/pkg/app/authorizer.go deleted file mode 100644 index 576773330e3..00000000000 --- a/apps/advisor/pkg/app/authorizer.go +++ /dev/null @@ -1,47 +0,0 @@ -package app - -import ( - "context" - - claims "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "k8s.io/apiserver/pkg/authorization/authorizer" -) - -func GetAuthorizer() authorizer.Authorizer { - return authorizer.AuthorizerFunc(func( - ctx context.Context, attr authorizer.Attributes, - ) (authorized authorizer.Decision, reason string, err error) { - if !attr.IsResourceRequest() { - return authorizer.DecisionNoOpinion, "", nil - } - - // Check for service identity - if identity.IsServiceIdentity(ctx) { - return authorizer.DecisionAllow, "", nil - } - - // Check for access policy identity - info, ok := claims.AuthInfoFrom(ctx) - if ok && claims.IsIdentityType(info.GetIdentityType(), claims.TypeAccessPolicy) { - // For access policy identities, we need to use ResourceAuthorizer - // This requires an AccessClient, which should be provided by the API server - // For now, we'll use the default ResourceAuthorizer from the API server - // This will be set up by the API server's authorization chain - return authorizer.DecisionNoOpinion, "", nil - } - - // For regular Grafana users, check if they are admin - u, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "valid user is required", err - } - - // check if is admin - if u.HasRole(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - - return authorizer.DecisionDeny, "forbidden", nil - }) -} diff --git a/apps/advisor/pkg/app/authorizer_test.go b/apps/advisor/pkg/app/authorizer_test.go deleted file mode 100644 index de8c0fad2db..00000000000 --- a/apps/advisor/pkg/app/authorizer_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package app - -import ( - "context" - "testing" - - claims "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/stretchr/testify/assert" - "k8s.io/apiserver/pkg/authorization/authorizer" -) - -func TestGetAuthorizer(t *testing.T) { - tests := []struct { - name string - ctx context.Context - attr authorizer.Attributes - expectedDecision authorizer.Decision - expectedReason string - expectedErr error - }{ - { - name: "non-resource request", - ctx: context.TODO(), - attr: &mockAttributes{resourceRequest: false}, - expectedDecision: authorizer.DecisionNoOpinion, - expectedReason: "", - expectedErr: nil, - }, - { - name: "user is admin", - ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: true}), - attr: &mockAttributes{resourceRequest: true}, - expectedDecision: authorizer.DecisionAllow, - expectedReason: "", - expectedErr: nil, - }, - { - name: "user is not admin", - ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: false}), - attr: &mockAttributes{resourceRequest: true}, - expectedDecision: authorizer.DecisionDeny, - expectedReason: "forbidden", - expectedErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - auth := GetAuthorizer() - decision, reason, err := auth.Authorize(tt.ctx, tt.attr) - assert.Equal(t, tt.expectedDecision, decision) - assert.Equal(t, tt.expectedReason, reason) - assert.Equal(t, tt.expectedErr, err) - }) - } -} - -type mockAttributes struct { - authorizer.Attributes - resourceRequest bool -} - -func (m *mockAttributes) IsResourceRequest() bool { - return m.resourceRequest -} - -// Implement other methods of authorizer.Attributes as needed - -type mockUser struct { - identity.Requester - isGrafanaAdmin bool -} - -func (m *mockUser) GetIsGrafanaAdmin() bool { - return m.isGrafanaAdmin -} - -func (m *mockUser) HasRole(role identity.RoleType) bool { - return role == identity.RoleAdmin && m.isGrafanaAdmin -} - -func (m *mockUser) GetUID() string { - return "test-uid" -} - -func (m *mockUser) GetIdentityType() claims.IdentityType { - return claims.TypeUser -} - -// Implement other methods of identity.Requester as needed diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index bb020e52bff..792ad18eeac 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,38 +3,48 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.7.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/kr/pretty v0.3.1 // 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/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.47.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index ef36568611c..9b83516bea0 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -8,46 +9,73 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= 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= -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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= @@ -73,8 +101,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -83,8 +111,8 @@ 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/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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -93,8 +121,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -102,16 +130,16 @@ 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/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 21ad42c90af..a79829d45c2 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -4,17 +4,17 @@ go 1.25.5 require ( github.com/go-kit/log v0.2.1 - github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba + github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/prometheus/client_golang v1.23.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( @@ -28,11 +28,11 @@ require ( github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.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 @@ -42,11 +42,21 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // 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/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/strfmt v0.24.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect @@ -57,7 +67,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // 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 @@ -80,7 +90,7 @@ require ( 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/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -104,10 +114,10 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/alertmanager v0.28.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // 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/prometheus/procfs v0.19.2 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -117,7 +127,7 @@ require ( github.com/stretchr/objx v0.5.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/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -130,49 +140,49 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp 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/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.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.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 - go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.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.45.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect golang.org/x/tools/godoc v0.1.0-deprecated // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index b4d2d1dc2e2..d45d418dfb8 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -63,8 +63,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 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.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -80,8 +80,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -123,16 +123,42 @@ 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/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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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 v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 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= @@ -140,7 +166,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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -187,8 +212,8 @@ 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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -216,16 +241,16 @@ 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/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= 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.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0= github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -301,8 +326,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 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= @@ -376,8 +401,8 @@ 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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= @@ -387,8 +412,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -431,8 +456,8 @@ 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/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= -github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -457,8 +482,8 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuP 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -469,12 +494,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9 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/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= 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= @@ -486,20 +511,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7i 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.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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 v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 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/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= -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/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -516,8 +541,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -552,8 +577,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -585,16 +610,16 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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= @@ -605,8 +630,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -644,19 +669,19 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -706,8 +731,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -774,10 +799,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -804,8 +829,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -816,8 +841,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= @@ -840,26 +865,26 @@ 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.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -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/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/alerting/historian/pkg/app/notification/lokireader.go b/apps/alerting/historian/pkg/app/notification/lokireader.go index c26519e59b4..636ae083d91 100644 --- a/apps/alerting/historian/pkg/app/notification/lokireader.go +++ b/apps/alerting/historian/pkg/app/notification/lokireader.go @@ -31,6 +31,10 @@ const ( maxLimit = 1000 Namespace = "grafana" Subsystem = "alerting" + + // LogQL field path for alert rule UID after JSON parsing. + // Loki flattens nested JSON fields with underscores: alert.labels.__alert_rule_uid__ -> alert_labels___alert_rule_uid__ + lokiAlertRuleUIDField = "alert_labels___alert_rule_uid__" ) var ( @@ -111,13 +115,13 @@ func buildQuery(query Query) (string, error) { fmt.Sprintf(`%s=%q`, historian.LabelFrom, historian.LabelFromValue), } - if query.RuleUID != nil { - selectors = append(selectors, - fmt.Sprintf(`%s=%q`, historian.LabelRuleUID, *query.RuleUID)) - } - logql := fmt.Sprintf(`{%s} | json`, strings.Join(selectors, `,`)) + // Add ruleUID filter as JSON line filter if specified. + if query.RuleUID != nil && *query.RuleUID != "" { + logql += fmt.Sprintf(` | %s = %q`, lokiAlertRuleUIDField, *query.RuleUID) + } + // Add receiver filter if specified. if query.Receiver != nil && *query.Receiver != "" { logql += fmt.Sprintf(` | receiver = %q`, *query.Receiver) @@ -211,16 +215,13 @@ func parseLokiEntry(s lokiclient.Sample) (Entry, error) { groupLabels = make(map[string]string) } - alerts := make([]EntryAlert, len(lokiEntry.Alerts)) - for i, a := range lokiEntry.Alerts { - alerts[i] = EntryAlert{ - Status: a.Status, - Labels: a.Labels, - Annotations: a.Annotations, - StartsAt: a.StartsAt, - EndsAt: a.EndsAt, - } - } + alerts := []EntryAlert{{ + Status: lokiEntry.Alert.Status, + Labels: lokiEntry.Alert.Labels, + Annotations: lokiEntry.Alert.Annotations, + StartsAt: lokiEntry.Alert.StartsAt, + EndsAt: lokiEntry.Alert.EndsAt, + }} return Entry{ Timestamp: s.T, diff --git a/apps/alerting/historian/pkg/app/notification/lokireader_test.go b/apps/alerting/historian/pkg/app/notification/lokireader_test.go index 708c9d10df1..c9c35cb1e62 100644 --- a/apps/alerting/historian/pkg/app/notification/lokireader_test.go +++ b/apps/alerting/historian/pkg/app/notification/lokireader_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify/historian" "github.com/grafana/alerting/notify/historian/lokiclient" "github.com/grafana/grafana-app-sdk/logging" @@ -133,9 +134,8 @@ func TestBuildQuery(t *testing.T) { query: Query{ RuleUID: stringPtr("test-rule-uid"), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid"`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with receiver filter", @@ -143,9 +143,8 @@ func TestBuildQuery(t *testing.T) { RuleUID: stringPtr("test-rule-uid"), Receiver: stringPtr("email-receiver"), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | receiver = "email-receiver"`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | receiver = "email-receiver"`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with status filter", @@ -153,9 +152,8 @@ func TestBuildQuery(t *testing.T) { RuleUID: stringPtr("test-rule-uid"), Status: createStatusPtr(v0alpha1.CreateNotificationqueryRequestNotificationStatusFiring), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | status = "firing"`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | status = "firing"`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with success outcome filter", @@ -163,9 +161,8 @@ func TestBuildQuery(t *testing.T) { RuleUID: stringPtr("test-rule-uid"), Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeSuccess), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | error = ""`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | error = ""`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with error outcome filter", @@ -173,9 +170,8 @@ func TestBuildQuery(t *testing.T) { RuleUID: stringPtr("test-rule-uid"), Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeError), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | error != ""`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | error != ""`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with many filters", @@ -185,9 +181,8 @@ func TestBuildQuery(t *testing.T) { Status: createStatusPtr(v0alpha1.CreateNotificationqueryRequestNotificationStatusResolved), Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeSuccess), }, - expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | receiver = "email-receiver" | status = "resolved" | error = ""`, - historian.LabelFrom, historian.LabelFromValue, - historian.LabelRuleUID, "test-rule-uid"), + expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | receiver = "email-receiver" | status = "resolved" | error = ""`, + historian.LabelFrom, historian.LabelFromValue), }, { name: "query with group label matcher", @@ -277,19 +272,19 @@ func TestParseLokiEntry(t *testing.T) { GroupLabels: map[string]string{ "alertname": "test-alert", }, - Alerts: []historian.NotificationHistoryLokiEntryAlert{ - { - Status: "firing", - Labels: map[string]string{ - "severity": "critical", - }, - Annotations: map[string]string{ - "summary": "Test alert", - }, - StartsAt: now, - EndsAt: now.Add(1 * time.Hour), + Alert: historian.NotificationHistoryLokiEntryAlert{ + Status: "firing", + Labels: map[string]string{ + "severity": "critical", }, + Annotations: map[string]string{ + "summary": "Test alert", + }, + StartsAt: now, + EndsAt: now.Add(1 * time.Hour), }, + AlertIndex: 0, + AlertCount: 1, Retry: false, Duration: 100, PipelineTime: now, @@ -335,7 +330,9 @@ func TestParseLokiEntry(t *testing.T) { Error: "notification failed", GroupKey: "key:thing", GroupLabels: map[string]string{}, - Alerts: []historian.NotificationHistoryLokiEntryAlert{}, + Alert: historian.NotificationHistoryLokiEntryAlert{}, + AlertIndex: 0, + AlertCount: 1, PipelineTime: now, }), }, @@ -347,7 +344,7 @@ func TestParseLokiEntry(t *testing.T) { Outcome: OutcomeError, GroupKey: "key:thing", GroupLabels: map[string]string{}, - Alerts: []EntryAlert{}, + Alerts: []EntryAlert{{}}, Error: stringPtr("notification failed"), PipelineTime: now, }, @@ -365,7 +362,7 @@ func TestParseLokiEntry(t *testing.T) { Status: Status("firing"), Outcome: OutcomeSuccess, GroupLabels: map[string]string{}, - Alerts: []EntryAlert{}, + Alerts: []EntryAlert{{}}, PipelineTime: now, }, }, @@ -448,7 +445,9 @@ func TestLokiReader_RunQuery(t *testing.T) { Receiver: "receiver-1", Status: "firing", GroupLabels: map[string]string{}, - Alerts: []historian.NotificationHistoryLokiEntryAlert{}, + Alert: historian.NotificationHistoryLokiEntryAlert{}, + AlertIndex: 0, + AlertCount: 1, PipelineTime: now, }), }, @@ -459,7 +458,9 @@ func TestLokiReader_RunQuery(t *testing.T) { Receiver: "receiver-3", Status: "firing", GroupLabels: map[string]string{}, - Alerts: []historian.NotificationHistoryLokiEntryAlert{}, + Alert: historian.NotificationHistoryLokiEntryAlert{}, + AlertIndex: 0, + AlertCount: 1, PipelineTime: now, }), }, @@ -474,7 +475,9 @@ func TestLokiReader_RunQuery(t *testing.T) { Receiver: "receiver-2", Status: "firing", GroupLabels: map[string]string{}, - Alerts: []historian.NotificationHistoryLokiEntryAlert{}, + Alert: historian.NotificationHistoryLokiEntryAlert{}, + AlertIndex: 0, + AlertCount: 1, PipelineTime: now, }), }, @@ -546,19 +549,19 @@ func createMockLokiResponse(timestamp time.Time) lokiclient.QueryRes { GroupLabels: map[string]string{ "alertname": "test-alert", }, - Alerts: []historian.NotificationHistoryLokiEntryAlert{ - { - Status: "firing", - Labels: map[string]string{ - "severity": "critical", - }, - Annotations: map[string]string{ - "summary": "Test alert", - }, - StartsAt: timestamp, - EndsAt: timestamp.Add(1 * time.Hour), + Alert: historian.NotificationHistoryLokiEntryAlert{ + Status: "firing", + Labels: map[string]string{ + "severity": "critical", }, + Annotations: map[string]string{ + "summary": "Test alert", + }, + StartsAt: timestamp, + EndsAt: timestamp.Add(1 * time.Hour), }, + AlertIndex: 0, + AlertCount: 1, Retry: false, Duration: 100, PipelineTime: timestamp, @@ -587,10 +590,19 @@ func createLokiEntryJSONWithNilLabels(t *testing.T, timestamp time.Time) string "status": "firing", "error": "", "groupLabels": null, - "alerts": [], + "alert": {}, + "alertIndex": 0, + "alertCount": 1, "retry": false, "duration": 0, "pipelineTime": "%s" }`, timestamp.Format(time.RFC3339Nano)) return jsonStr } + +func TestRuleUIDLabelConstant(t *testing.T) { + // Verify that models.RuleUIDLabel has the expected value. + // If this changes in the alerting module, our LogQL field path constant will be incorrect + // and filtering for a single alert rule by its UID will break. + assert.Equal(t, "__alert_rule_uid__", models.RuleUIDLabel) +} diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index f72c4c9775b..b958cb377aa 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,21 +3,21 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.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 @@ -26,14 +26,24 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // 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 @@ -44,7 +54,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -54,57 +64,56 @@ require ( 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/pkg/errors v0.9.1 // 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // 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.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/component-base v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 57b099ab2f1..ea54639af1d 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -4,8 +4,8 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -13,8 +13,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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= @@ -36,19 +36,44 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -60,8 +85,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -71,10 +96,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -104,8 +129,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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= @@ -127,8 +152,6 @@ 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= @@ -140,11 +163,11 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -165,8 +188,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7 github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= @@ -175,12 +198,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.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.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= -go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= -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/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= +go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= 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= @@ -189,26 +212,26 @@ 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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/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/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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -224,8 +247,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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,30 +259,30 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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-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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -267,8 +290,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -279,20 +302,20 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -300,28 +323,28 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/alerting/notifications/kinds/v0alpha1/template_spec.cue b/apps/alerting/notifications/kinds/v0alpha1/template_spec.cue index 4c3157f39a4..297d3331eeb 100644 --- a/apps/alerting/notifications/kinds/v0alpha1/template_spec.cue +++ b/apps/alerting/notifications/kinds/v0alpha1/template_spec.cue @@ -1,6 +1,9 @@ package v0alpha1 +TemplateKind: *"grafana" | "mimir" + TemplateGroupSpec: { title: string content: string + kind: TemplateKind } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go index ecba2959665..2f4fee39b9e 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go @@ -2,13 +2,24 @@ package v0alpha1 +// +k8s:openapi-gen=true +type TemplateGroupTemplateKind string + +const ( + TemplateGroupTemplateKindGrafana TemplateGroupTemplateKind = "grafana" + TemplateGroupTemplateKindMimir TemplateGroupTemplateKind = "mimir" +) + // +k8s:openapi-gen=true type TemplateGroupSpec struct { - Title string `json:"title"` - Content string `json:"content"` + Title string `json:"title"` + Content string `json:"content"` + Kind TemplateGroupTemplateKind `json:"kind"` } // NewTemplateGroupSpec creates a new TemplateGroupSpec object. func NewTemplateGroupSpec() *TemplateGroupSpec { - return &TemplateGroupSpec{} + return &TemplateGroupSpec{ + Kind: TemplateGroupTemplateKindGrafana, + } } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index e16f443a4f6..18447f62760 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -26,7 +26,7 @@ var ( 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(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"}}`) + rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TemplateKind":{"enum":["grafana","mimir"],"type":"string"},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"kind":{"$ref":"#/components/schemas/TemplateKind","default":"grafana"},"title":{"type":"string"}},"required":["title","content","kind"],"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"},"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"}}`) diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 63da00536a6..45028a5bb0f 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,16 +3,16 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - github.com/prometheus/common v0.67.3 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + github.com/prometheus/common v0.67.4 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -22,13 +22,23 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -37,7 +47,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -50,42 +60,42 @@ 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/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 946c42fe38f..aa4942ccd60 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/apps/annotation go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -21,13 +21,23 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -36,7 +46,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -49,43 +59,43 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 00575d7e6d4..05b9e33db99 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/grafana/apps/collections go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( @@ -18,19 +18,29 @@ require ( 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-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/grafana/grafana-app-sdk/logging v0.48.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // 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/mailru/easyjson v0.9.1 // 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 @@ -41,28 +51,28 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 75a19848d73..aa5de8cbe99 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -14,29 +14,55 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -58,8 +84,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -76,8 +102,6 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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= @@ -85,10 +109,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -101,16 +125,16 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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/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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -126,24 +150,24 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -154,31 +178,31 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/collections/pkg/apis/collections/v1alpha1/stars.go b/apps/collections/pkg/apis/collections/v1alpha1/stars.go index e203e6b560e..79bff8a6513 100644 --- a/apps/collections/pkg/apis/collections/v1alpha1/stars.go +++ b/apps/collections/pkg/apis/collections/v1alpha1/stars.go @@ -8,9 +8,8 @@ import ( func (stars *StarsSpec) Add(group, kind, name string) { for i, r := range stars.Resource { if r.Group == group && r.Kind == kind { - r.Names = append(r.Names, name) - slices.Sort(r.Names) - stars.Resource[i].Names = slices.Compact(r.Names) + stars.Resource[i].Names = append(r.Names, name) + stars.Normalize() return } } @@ -46,8 +45,15 @@ func (stars *StarsSpec) Normalize() { resources := make([]StarsResource, 0, len(stars.Resource)) for _, r := range stars.Resource { if len(r.Names) > 0 { - slices.Sort(r.Names) - r.Names = slices.Compact(r.Names) // removes any duplicates + unique := make([]string, 0, len(r.Names)) + found := make(map[string]bool, len(r.Names)) + for _, name := range r.Names { + if !found[name] { + unique = append(unique, name) + found[name] = true + } + } + r.Names = unique resources = append(resources, r) } } diff --git a/apps/collections/pkg/apis/collections/v1alpha1/stars_test.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_test.go index 5713b07a751..d4421fae614 100644 --- a/apps/collections/pkg/apis/collections/v1alpha1/stars_test.go +++ b/apps/collections/pkg/apis/collections/v1alpha1/stars_test.go @@ -39,7 +39,7 @@ func TestStarsWrite(t *testing.T) { Resource: []StarsResource{{ Group: "g", Kind: "k", - Names: []string{"a", "b", "c", "x"}, // added "b" (and sorted) + Names: []string{"a", "b", "x", "c"}, // added c to the end }}, }, }, { diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 29bc91e70a6..388afc2fd38 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/apps/correlations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -21,13 +21,23 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -36,7 +46,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -49,43 +59,43 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 0a19120fe80..86d5ec1ff09 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,16 +5,17 @@ go 1.25.5 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ) require ( @@ -33,17 +34,27 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect @@ -65,7 +76,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -79,55 +90,55 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace 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/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect 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.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/component-base v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 0faeaaf78ba..3bc70db5e6e 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -1,5 +1,5 @@ -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -28,8 +28,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 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/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -47,14 +47,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= @@ -71,8 +97,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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= @@ -85,10 +111,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -112,6 +138,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 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/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/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= @@ -140,8 +168,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -183,12 +211,12 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 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/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/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= @@ -202,12 +230,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -229,8 +257,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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= @@ -241,34 +269,34 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= 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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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.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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -280,25 +308,25 @@ 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/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/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -309,24 +337,24 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -335,42 +363,42 @@ 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/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 3fe93d7305f..c13eb866c80 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -768,6 +768,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -803,6 +807,7 @@ QueryVariableSpec: { datasource?: DataSourceRef query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index ef4a27fd6b7..bb833795354 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -806,6 +810,7 @@ QueryVariableSpec: { description?: string query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index d2a65bbbf24..8338c9e13c5 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -222,6 +222,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -249,6 +251,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting @@ -700,6 +706,10 @@ lineage: schemas: [{ // Field options allow you to change how the data is displayed in your visualizations. fieldConfig?: #FieldConfigSource + + // When a panel is migrated from a previous version (Angular to React), this field is set to the original panel type. + // This is used to determine the original panel type when migrating to a new version so the plugin migration can be applied. + autoMigrateFrom?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index d2a65bbbf24..8338c9e13c5 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -222,6 +222,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -249,6 +251,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting @@ -700,6 +706,10 @@ lineage: schemas: [{ // Field options allow you to change how the data is displayed in your visualizations. fieldConfig?: #FieldConfigSource + + // When a panel is migrated from a previous version (Angular to React), this field is set to the original panel type. + // This is used to determine the original panel type when migrating to a new version so the plugin migration can be applied. + autoMigrateFrom?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index d822ad9f38a..ec8d7eead87 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -807,6 +811,7 @@ QueryVariableSpec: { datasource?: DataSourceRef query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] 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 625c6fe17c0..883c5399663 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1364,6 +1364,7 @@ type DashboardQueryVariableSpec struct { Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` + RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"` Sort DashboardVariableSort `json:"sort"` Definition *string `json:"definition,omitempty"` Options []DashboardVariableOption `json:"options"` @@ -1393,6 +1394,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec { SkipUrlSync: false, Query: *NewDashboardDataQueryKind(), Regex: "", + RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue), Options: []DashboardVariableOption{}, Multi: false, IncludeAll: false, @@ -1443,6 +1445,16 @@ const ( DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged" ) +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +// +k8s:openapi-gen=true +type DashboardVariableRegexApplyTo string + +const ( + DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value" + DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text" +) + // Sort variable options // Accepted values are: // `disabled`: No sorting 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 ff7429d7c2b..d697aa4f8b9 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -3646,6 +3646,12 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re Format: "", }, }, + "regexApplyTo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "sort": { SchemaProps: spec.SchemaProps{ Default: "", diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 0c061075e6d..12d7bec351b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -776,6 +776,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -810,6 +814,7 @@ QueryVariableSpec: { description?: string query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] 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 a6e63aa0bcc..a8ec1537e38 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -1367,6 +1367,7 @@ type DashboardQueryVariableSpec struct { Description *string `json:"description,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` + RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"` Sort DashboardVariableSort `json:"sort"` Definition *string `json:"definition,omitempty"` Options []DashboardVariableOption `json:"options"` @@ -1396,6 +1397,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec { SkipUrlSync: false, Query: *NewDashboardDataQueryKind(), Regex: "", + RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue), Options: []DashboardVariableOption{}, Multi: false, IncludeAll: false, @@ -1447,6 +1449,16 @@ const ( DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged" ) +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +// +k8s:openapi-gen=true +type DashboardVariableRegexApplyTo string + +const ( + DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value" + DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text" +) + // Sort variable options // Accepted values are: // `disabled`: No sorting 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 3a129374192..2b1fe573336 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -3656,6 +3656,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.Ref Format: "", }, }, + "regexApplyTo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "sort": { SchemaProps: spec.SchemaProps{ Default: "", diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index c4e35bd8f40..c815e815e08 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -32,10 +32,10 @@ var ( rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv1beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) - rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) - rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) ) diff --git a/apps/dashboard/pkg/migration/README.md b/apps/dashboard/pkg/migration/README.md index ab938e31f66..6fd2069cce9 100644 --- a/apps/dashboard/pkg/migration/README.md +++ b/apps/dashboard/pkg/migration/README.md @@ -53,6 +53,7 @@ v0alpha1 (Legacy JSON) → v1beta1 (Migrated JSON) → v2alpha1/v2beta1 (Structu - Transforms JSON dashboards to structured dashboard format - v2 schema is the stable, typed schema with proper type definitions - Handles modern dashboard features and Kubernetes-native storage +- Preserves Angular panel migration data for frontend processing (see [Angular Panel Migrations](#angular-panel-migrations)) - See [V2 to V1 Layout Conversion](./conversion/v2_to_v1_layout_conversion.md) for details on how V2 layouts are converted back to V1 panel arrays #### v2 to v0/v1 Conversion: @@ -61,6 +62,76 @@ v0alpha1 (Legacy JSON) → v1beta1 (Migrated JSON) → v2alpha1/v2beta1 (Structu - v0alpha1 and v1beta1 share the same spec structure (only API version differs) - Enables backward compatibility when storing v2 dashboards in legacy format +### Angular Panel Migrations + +When converting dashboards from v0/v1 to v2, panels with Angular types require special handling. The `autoMigrateFrom` field is used in v0 and v1 to indicate the panel was migrated from a deprecated plugin type, and the target plugin contains migration logic to transform the original panel's options and field configurations. + +Panel plugins define their own migration logic via `plugin.onPanelTypeChanged()`. This migration runs in the frontend when a panel type changes, transforming old options/fieldConfig to the new format. Examples: +- `singlestat.format: "short"` → `stat.fieldConfig.defaults.unit: "short"` +- `graph.legend.show: true` → `timeseries.options.legend.showLegend: true` + +The v2 schema doesn't include `autoMigrateFrom` as a typed field, so we need a mechanism to preserve the original panel data for the frontend to run these plugin migrations. + +#### `__angularMigration` Temporary Data + +The backend v1 → v2 conversion preserves the original panel data in a temporary field within `vizConfig.spec.options`. This works for **any** Angular panel, not just specific panel types: + +```json +{ + "vizConfig": { + "kind": "stat", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalPanel": { + "type": "singlestat", + "format": "short", + "colorBackground": true, + "sparkline": { "show": true }, + "fieldConfig": { ... }, + "options": { ... } + } + } + } + } + } +} +``` + +#### How It Works + +1. **Backend (v1 → v2):** The conversion detects panels that need Angular migration in two ways: + - If `autoMigrateFrom` is already set on the panel (from v0 → v1 migration) - panel type already converted + - If the panel type is a known Angular panel type (fallback for dashboards stored directly as v1 without v0 → v1 migration) + + In the second case, the conversion also transforms the panel type (e.g., `singlestat` → `stat`) and sets `autoMigrateFrom`. This replicates the same logic as the v0 → v1 migration. + + The entire original panel is stored under `options.__angularMigration` for the frontend to run plugin-specific migrations. + +2. **Frontend (v2 load):** When building a `VizPanel` from v2 data: + - Extracts `__angularMigration` from options + - Removes it from the options object (not persisted) + - Attaches a custom migration handler via `_UNSAFE_customMigrationHandler` + +3. **Plugin load (VizPanel activation):** When the plugin loads, the migration handler calls `plugin.onPanelTypeChanged()` with the original panel data, allowing the plugin's own migration code to run + +#### Key Points + +- Works for **any** panel with `autoMigrateFrom`, not limited to specific panel types +- Each plugin defines its own migration logic - the backend just preserves the data +- The `originalPanel` contains the complete panel data to ensure no information is lost +- Migration data is removed from options after loaded in frontend +- The 0v → v1 and v1 → v2 conversions automatically detects these Angular panel types if `autoMigrateFrom` is not set. + +#### Implementation Files + +| File | Purpose | +|------|---------| +| `apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go` | Injects `__angularMigration` during v1 → v2 | +| `public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts` | Extracts and consumes `__angularMigration` | +| `public/app/features/dashboard-scene/serialization/angularMigration.ts` | Creates migration handler for v2 path | + ## Conversion Matrix The system supports conversions between all dashboard API versions: diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index d0f90e1ba98..ad65dc84c85 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -12,13 +12,6 @@ import ( ) func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { - // Wrap the provider once with 10s caching for all conversions. - // This prevents repeated DB queries across multiple conversion calls while allowing - // the cache to refresh periodically, making it suitable for long-lived singleton usage. - dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider) - // Wrap library element provider with caching as well - leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider) - // 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 { @@ -62,13 +55,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo // v2alpha1 conversions if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv0.Dashboard)(nil), withConversionMetrics(dashv2alpha1.APIVERSION, dashv0.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope, dsIndexProvider) + return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope) })); err != nil { return err } 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_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope, dsIndexProvider) + return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope) })); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go b/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go new file mode 100644 index 00000000000..3e54d9682fd --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go @@ -0,0 +1,454 @@ +package conversion + +import ( + "context" + "sync/atomic" + "testing" + "time" + + 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/schemaversion" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// countingDataSourceProvider tracks how many times Index() is called +type countingDataSourceProvider struct { + datasources []schemaversion.DataSourceInfo + callCount atomic.Int64 +} + +func newCountingDataSourceProvider(datasources []schemaversion.DataSourceInfo) *countingDataSourceProvider { + return &countingDataSourceProvider{ + datasources: datasources, + } +} + +func (p *countingDataSourceProvider) Index(_ context.Context) *schemaversion.DatasourceIndex { + p.callCount.Add(1) + return schemaversion.NewDatasourceIndex(p.datasources) +} + +func (p *countingDataSourceProvider) getCallCount() int64 { + return p.callCount.Load() +} + +// countingLibraryElementProvider tracks how many times GetLibraryElementInfo() is called +type countingLibraryElementProvider struct { + elements []schemaversion.LibraryElementInfo + callCount atomic.Int64 +} + +func newCountingLibraryElementProvider(elements []schemaversion.LibraryElementInfo) *countingLibraryElementProvider { + return &countingLibraryElementProvider{ + elements: elements, + } +} + +func (p *countingLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + p.callCount.Add(1) + return p.elements +} + +func (p *countingLibraryElementProvider) getCallCount() int64 { + return p.callCount.Load() +} + +// createTestV0Dashboard creates a minimal v0 dashboard for testing +// The dashboard has a datasource with UID only (no type) to force provider lookup +// and includes library panels to test library element provider caching +func createTestV0Dashboard(namespace, title string) *dashv0.Dashboard { + return &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dashboard", + Namespace: namespace, + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": title, + "schemaVersion": schemaversion.LATEST_VERSION, + // Variables with datasource reference that requires lookup + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "query_var", + "type": "query", + "query": "label_values(up, job)", + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + // type is intentionally omitted to trigger provider lookup + }, + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "targets": []interface{}{ + map[string]interface{}{ + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + }, + }, + }, + // Library panel reference - triggers library element provider lookup + map[string]interface{}{ + "id": 2, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 8, + "w": 12, + "x": 0, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat", + }, + }, + // Another library panel reference + map[string]interface{}{ + "id": 3, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 3, + "w": 6, + "x": 0, + "y": 16, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat", + }, + }, + }, + }, + }, + } +} + +// createTestV1Dashboard creates a minimal v1beta1 dashboard for testing +// The dashboard has a datasource with UID only (no type) to force provider lookup +// and includes library panels to test library element provider caching +func createTestV1Dashboard(namespace, title string) *dashv1.Dashboard { + return &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dashboard", + Namespace: namespace, + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": title, + "schemaVersion": schemaversion.LATEST_VERSION, + // Variables with datasource reference that requires lookup + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "query_var", + "type": "query", + "query": "label_values(up, job)", + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + // type is intentionally omitted to trigger provider lookup + }, + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "targets": []interface{}{ + map[string]interface{}{ + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + }, + }, + }, + // Library panel reference - triggers library element provider lookup + map[string]interface{}{ + "id": 2, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 4, + "w": 6, + "x": 0, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-repeat-v", + "name": "Library Panel with Vertical Repeat", + }, + }, + // Another library panel reference + map[string]interface{}{ + "id": 3, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 3, + "w": 6, + "x": 6, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat", + }, + }, + }, + }, + }, + } +} + +// TestConversionCaching_V0_to_V2alpha1 verifies caching works when converting V0 to V2alpha1 +func TestConversionCaching_V0_to_V2alpha1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + // Convert multiple dashboards in the same namespace + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + // With caching, the underlying datasource provider should only be called once per namespace + // The test dashboard has datasources without type that require lookup + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + // Library element provider should also be called only once per namespace due to caching + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V0_to_V2beta1 verifies caching works when converting V0 to V2beta1 +func TestConversionCaching_V0_to_V2beta1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V1beta1_to_V2alpha1 verifies caching works when converting V1beta1 to V2alpha1 +func TestConversionCaching_V1beta1_to_V2alpha1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V1beta1_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V1beta1_to_V2beta1 verifies caching works when converting V1beta1 to V2beta1 +func TestConversionCaching_V1beta1_to_V2beta1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2beta1.Dashboard{} + + err := Convert_V1beta1_to_V2beta1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_MultipleNamespaces verifies that different namespaces get separate cache entries +func TestConversionCaching_MultipleNamespaces(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + namespaces := []string{"default", "org-2", "org-3"} + numDashboardsPerNs := 3 + + for _, ns := range namespaces { + for i := 0; i < numDashboardsPerNs; i++ { + source := createTestV0Dashboard(ns, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion for namespace %s should succeed", ns) + } + } + + // With caching, each namespace should result in one call to the underlying provider + expectedCalls := int64(len(namespaces)) + assert.Equal(t, expectedCalls, underlyingDS.getCallCount(), + "datasource provider should be called once per namespace (%d namespaces)", len(namespaces)) + assert.Equal(t, expectedCalls, underlyingLE.getCallCount(), + "library element provider should be called once per namespace (%d namespaces)", len(namespaces)) +} + +// TestConversionCaching_CacheDisabled verifies that TTL=0 disables caching +func TestConversionCaching_CacheDisabled(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + // TTL of 0 should disable caching - the wrapper returns the underlying provider directly + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, 0) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, 0) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 3 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + } + + // Without caching, each conversion calls the underlying provider multiple times + // (once for each datasource lookup needed - variables and panels) + // The key check is that the count is GREATER than 1 per conversion (no caching benefit) + assert.Greater(t, underlyingDS.getCallCount(), int64(numDashboards), + "with cache disabled, conversions should call datasource provider multiple times") + // Library element provider is also called for each conversion without caching + assert.GreaterOrEqual(t, underlyingLE.getCallCount(), int64(numDashboards), + "with cache disabled, conversions should call library element provider multiple times") +} diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go index 296a95ac054..269fb51bd70 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go @@ -4,8 +4,6 @@ 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" @@ -121,6 +119,14 @@ func countPanelsV0V1(spec map[string]interface{}) int { return count } +// countTargetsFromPanel counts the number of targets/queries in a panel. +func countTargetsFromPanel(panelMap map[string]interface{}) int { + if targets, ok := panelMap["targets"].([]interface{}); ok { + return len(targets) + } + return 0 +} + // 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. @@ -145,9 +151,7 @@ func countQueriesV0V1(spec map[string]interface{}) int { // Count queries in regular panels (NOT row panels) if panelType != "row" { - if targets, ok := panelMap["targets"].([]interface{}); ok { - count += len(targets) - } + count += countTargetsFromPanel(panelMap) } // Count queries in collapsed panels inside row panels @@ -155,9 +159,7 @@ func countQueriesV0V1(spec map[string]interface{}) int { 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) - } + count += countTargetsFromPanel(cpMap) } } } @@ -178,12 +180,15 @@ func countAnnotationsV0V1(spec map[string]interface{}) int { return 0 } - annotationList, ok := annotations["list"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if annotationList, ok := annotations["list"].([]interface{}); ok { + return len(annotationList) + } + if annotationList, ok := annotations["list"].([]map[string]interface{}); ok { + return len(annotationList) } - return len(annotationList) + return 0 } // countLinksV0V1 counts dashboard links in v0alpha1 or v1beta1 dashboard spec @@ -192,12 +197,15 @@ func countLinksV0V1(spec map[string]interface{}) int { return 0 } - links, ok := spec["links"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if links, ok := spec["links"].([]interface{}); ok { + return len(links) + } + if links, ok := spec["links"].([]map[string]interface{}); ok { + return len(links) } - return len(links) + return 0 } // countVariablesV0V1 counts template variables in v0alpha1 or v1beta1 dashboard spec @@ -211,12 +219,15 @@ func countVariablesV0V1(spec map[string]interface{}) int { return 0 } - variableList, ok := templating["list"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if variableList, ok := templating["list"].([]interface{}); ok { + return len(variableList) + } + if variableList, ok := templating["list"].([]map[string]interface{}); ok { + return len(variableList) } - return len(variableList) + return 0 } // collectStatsV0V1 collects statistics from v0alpha1 or v1beta1 dashboard @@ -442,77 +453,3 @@ func collectDashboardStats(dashboard interface{}) dashboardStats { } 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 index f49305996f9..5c28c98b861 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go @@ -410,158 +410,167 @@ func TestCountPanelsV2(t *testing.T) { } 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, + // Tests that expect NO error (no data loss) + t.Run("success cases", func(t *testing.T) { + successTests := []struct { + name string + sourceStats dashboardStats + targetStats dashboardStats + }{ + { + 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, + }, }, - targetStats: dashboardStats{ - panelCount: 3, - queryCount: 5, - annotationCount: 2, - linkCount: 1, - variableCount: 2, + { + 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: "panel count decreased (data loss)", - sourceStats: dashboardStats{ - panelCount: 3, - queryCount: 5, - annotationCount: 2, - linkCount: 1, + { + 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, + }, }, - 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 { + for _, tt := range successTests { + t.Run(tt.name, func(t *testing.T) { + err := detectConversionDataLoss(tt.sourceStats, tt.targetStats, "TestSource", "TestTarget") + assert.NoError(t, err) + }) + } + }) + + // Tests that expect an error (data loss detected) + t.Run("data loss cases", func(t *testing.T) { + errorTests := []struct { + name string + sourceStats dashboardStats + targetStats dashboardStats + errorMsg string + }{ + { + 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, + }, + errorMsg: "panel count decreased", + }, + { + 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, + }, + 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, + }, + errorMsg: "annotation count decreased", + }, + { + 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! + }, + 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, + }, + errorMsg: "panel count decreased", + }, + } + + for _, tt := range errorTests { + t.Run(tt.name, func(t *testing.T) { + err := detectConversionDataLoss(tt.sourceStats, tt.targetStats, "TestSource", "TestTarget") require.Error(t, err) assert.Contains(t, err.Error(), tt.errorMsg) @@ -569,11 +578,9 @@ func TestDetectConversionDataLoss(t *testing.T) { 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) { @@ -687,140 +694,692 @@ func TestConversionDataLossError(t *testing.T) { 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"}, - }, - }, - }, - } +// TestWithConversionMetrics_DataLossOnlyCheckedOnSuccess verifies that the +// withConversionMetrics wrapper only runs data loss detection when conversion succeeds. +func TestWithConversionMetrics_DataLossOnlyCheckedOnSuccess(t *testing.T) { + conversionError := errors.New("conversion failed") - // 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"}, - }, - }, - }, - } + // Test 1: Conversion fails - data loss should NOT be checked + t.Run("data loss not checked when conversion fails", func(t *testing.T) { + mockFailingConversion := func(a, b interface{}, scope conversion.Scope) error { + return conversionError + } - // 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 - } + wrappedFunc := withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, mockFailingConversion) - // 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", + 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"}, }, - 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 + targetV1 := &dashv1.Dashboard{} // Empty - would trigger data loss if checked + + // Execute - withConversionMetrics always returns nil to avoid 500s + err := wrappedFunc(sourceV0, targetV1, nil) + require.NoError(t, err, "withConversionMetrics should return nil even on conversion failure") + }) + + // Test 2: Conversion succeeds but data loss occurs - should detect data loss + t.Run("data loss detected when conversion succeeds but loses data", func(t *testing.T) { + mockDataLosingConversion := func(a, b interface{}, scope conversion.Scope) error { + // Conversion "succeeds" but loses a panel + target := b.(*dashv1.Dashboard) + target.Spec = dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + // Only 1 panel instead of 2 + map[string]interface{}{"id": float64(1), "type": "graph"}, + }, + }, + } + return nil + } + + wrappedFunc := withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, mockDataLosingConversion) + + 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{} + + // Execute - withConversionMetrics always returns nil + err := wrappedFunc(sourceV0, targetV1, nil) + require.NoError(t, err, "withConversionMetrics should return nil even on data loss") + + // The data loss is logged and recorded in metrics, but no error is returned to API server + }) +} + +// TestDataLossDetectionReturnsCorrectError verifies that when data loss is detected, +// a ConversionDataLossError is returned with proper function name and details. +func TestDataLossDetectionReturnsCorrectError(t *testing.T) { + t.Run("panel loss returns ConversionDataLossError", func(t *testing.T) { + 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{ + Spec: dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + // Missing panel 2 + map[string]interface{}{"id": float64(1), "type": "graph"}, + }, + }, + }, + } + + err := checkConversionDataLoss(dashv0.APIVERSION, dashv1.APIVERSION, sourceV0, targetV1) + + require.Error(t, err) + var dataLossErr *ConversionDataLossError + require.ErrorAs(t, err, &dataLossErr) + assert.Equal(t, "V0_to_V1", dataLossErr.GetFunctionName()) + assert.Contains(t, err.Error(), "panel count decreased") + }) + + t.Run("query loss returns ConversionDataLossError", func(t *testing.T) { + 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{}{"refId": "B"}, + }, + }, + }, + }, + }, + } + + targetV1 := &dashv1.Dashboard{ + Spec: dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + // Missing query B + map[string]interface{}{"refId": "A"}, + }, + }, + }, + }, + }, + } + + err := checkConversionDataLoss(dashv0.APIVERSION, dashv1.APIVERSION, sourceV0, targetV1) + + require.Error(t, err) + var dataLossErr *ConversionDataLossError + require.ErrorAs(t, err, &dataLossErr) + assert.Contains(t, err.Error(), "query count decreased") + }) + + t.Run("no data loss returns nil", func(t *testing.T) { + 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"}, + }, + }, + }, + }, + }, + } + + 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"}, + }, + }, + }, + }, + }, + } + + err := checkConversionDataLoss(dashv0.APIVERSION, dashv1.APIVERSION, sourceV0, targetV1) + + require.NoError(t, err) + }) +} + +// TestDataLossDetection_AllTypesAllVersions verifies that data loss is detected for +// all types of loss (panels, queries, annotations, links, variables) across all +// version permutations (v0alpha1, v1beta1, v2alpha1, v2beta1). +func TestDataLossDetection_AllTypesAllVersions(t *testing.T) { + // Helper to create v0/v1 unstructured spec with data + createV0V1SpecWithData := func() map[string]interface{} { + return 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": "C"}, + }, + }, + }, + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + map[string]interface{}{"name": "Annotation 2"}, + }, + }, + "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"}, + }, + }, + } } - // Wrap with data loss detection - validatedFunc := withConversionDataLossDetection("V0", "V1beta1", mockBadConversion) + // Helper to create v0/v1 unstructured spec with data loss (missing items) + createV0V1SpecWithLoss := func(lossType string) map[string]interface{} { + spec := createV0V1SpecWithData() + switch lossType { + case "panel": + spec["panels"] = []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + // Missing panel 2 + } + case "query": + spec["panels"] = []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + // Missing query B + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "C"}, + }, + }, + } + case "annotation": + spec["annotations"] = map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + // Missing annotation 2 + }, + } + case "link": + spec["links"] = []interface{}{ + map[string]interface{}{"title": "Link 1"}, + // Missing link 2 + } + case "variable": + spec["templating"] = map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "var1", "type": "query"}, + // Missing var2 + }, + } + } + return spec + } - // This should fail due to data loss - err := validatedFunc(sourceV0, targetV1, nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "panel count decreased") + // Helper to create v2alpha1 dashboard with data + createV2alpha1WithData := func() *dashv2alpha1.Dashboard { + return &dashv2alpha1.Dashboard{ + 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{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "B"}}, + }, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "C"}}, + }, + }, + }, + }, + }, + }, + }, + Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{{}, {}}, + Links: []dashv2alpha1.DashboardDashboardLink{{}, {}}, + Variables: []dashv2alpha1.DashboardVariableKind{{}, {}}, + }, + } + } - var detectConversionDataLossErr *ConversionDataLossError - require.ErrorAs(t, err, &detectConversionDataLossErr) - assert.Equal(t, "V0_to_V1beta1", detectConversionDataLossErr.GetFunctionName()) + // Helper to create v2alpha1 dashboard with data loss + createV2alpha1WithLoss := func(lossType string) *dashv2alpha1.Dashboard { + dash := createV2alpha1WithData() + switch lossType { + case "panel": + delete(dash.Spec.Elements, "panel2") + case "query": + dash.Spec.Elements["panel1"].PanelKind.Spec.Data.Spec.Queries = []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + // Missing query B + } + case "annotation": + dash.Spec.Annotations = []dashv2alpha1.DashboardAnnotationQueryKind{{}} + case "link": + dash.Spec.Links = []dashv2alpha1.DashboardDashboardLink{{}} + case "variable": + dash.Spec.Variables = []dashv2alpha1.DashboardVariableKind{{}} + } + return dash + } + + // Helper to create v2beta1 dashboard with data + createV2beta1WithData := func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Elements: map[string]dashv2beta1.DashboardElement{ + "panel1": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 1, + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "A"}}, + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "B"}}, + }, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 2, + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "C"}}, + }, + }, + }, + }, + }, + }, + }, + Annotations: []dashv2beta1.DashboardAnnotationQueryKind{{}, {}}, + Links: []dashv2beta1.DashboardDashboardLink{{}, {}}, + Variables: []dashv2beta1.DashboardVariableKind{{}, {}}, + }, + } + } + + // Helper to create v2beta1 dashboard with data loss + createV2beta1WithLoss := func(lossType string) *dashv2beta1.Dashboard { + dash := createV2beta1WithData() + switch lossType { + case "panel": + delete(dash.Spec.Elements, "panel2") + case "query": + dash.Spec.Elements["panel1"].PanelKind.Spec.Data.Spec.Queries = []dashv2beta1.DashboardPanelQueryKind{ + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "A"}}, + // Missing query B + } + case "annotation": + dash.Spec.Annotations = []dashv2beta1.DashboardAnnotationQueryKind{{}} + case "link": + dash.Spec.Links = []dashv2beta1.DashboardDashboardLink{{}} + case "variable": + dash.Spec.Variables = []dashv2beta1.DashboardVariableKind{{}} + } + return dash + } + + // All loss types to test + lossTypes := []struct { + name string + expectedErrMsg string + }{ + {"panel", "panel count decreased"}, + {"query", "query count decreased"}, + {"annotation", "annotation count decreased"}, + {"link", "link count decreased"}, + {"variable", "variable count decreased"}, + } + + // All version permutations (12 total: 4 versions × 3 targets each) + versionPairs := []struct { + sourceVersion string + targetVersion string + sourceAPI string + targetAPI string + }{ + // From v0alpha1 + {"v0alpha1", "v1beta1", dashv0.APIVERSION, dashv1.APIVERSION}, + {"v0alpha1", "v2alpha1", dashv0.APIVERSION, dashv2alpha1.APIVERSION}, + {"v0alpha1", "v2beta1", dashv0.APIVERSION, dashv2beta1.APIVERSION}, + // From v1beta1 + {"v1beta1", "v0alpha1", dashv1.APIVERSION, dashv0.APIVERSION}, + {"v1beta1", "v2alpha1", dashv1.APIVERSION, dashv2alpha1.APIVERSION}, + {"v1beta1", "v2beta1", dashv1.APIVERSION, dashv2beta1.APIVERSION}, + // From v2alpha1 + {"v2alpha1", "v0alpha1", dashv2alpha1.APIVERSION, dashv0.APIVERSION}, + {"v2alpha1", "v1beta1", dashv2alpha1.APIVERSION, dashv1.APIVERSION}, + {"v2alpha1", "v2beta1", dashv2alpha1.APIVERSION, dashv2beta1.APIVERSION}, + // From v2beta1 + {"v2beta1", "v0alpha1", dashv2beta1.APIVERSION, dashv0.APIVERSION}, + {"v2beta1", "v1beta1", dashv2beta1.APIVERSION, dashv1.APIVERSION}, + {"v2beta1", "v2alpha1", dashv2beta1.APIVERSION, dashv2alpha1.APIVERSION}, + } + + for _, vp := range versionPairs { + for _, lt := range lossTypes { + testName := fmt.Sprintf("%s_to_%s_%s_loss", vp.sourceVersion, vp.targetVersion, lt.name) + t.Run(testName, func(t *testing.T) { + var source, target interface{} + + // Create source dashboard with full data + switch vp.sourceVersion { + case "v0alpha1": + source = &dashv0.Dashboard{Spec: dashv0.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v1beta1": + source = &dashv1.Dashboard{Spec: dashv1.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v2alpha1": + source = createV2alpha1WithData() + case "v2beta1": + source = createV2beta1WithData() + } + + // Create target dashboard with data loss + switch vp.targetVersion { + case "v0alpha1": + target = &dashv0.Dashboard{Spec: dashv0.DashboardSpec{Object: createV0V1SpecWithLoss(lt.name)}} + case "v1beta1": + target = &dashv1.Dashboard{Spec: dashv1.DashboardSpec{Object: createV0V1SpecWithLoss(lt.name)}} + case "v2alpha1": + target = createV2alpha1WithLoss(lt.name) + case "v2beta1": + target = createV2beta1WithLoss(lt.name) + } + + // Check for data loss + err := checkConversionDataLoss(vp.sourceAPI, vp.targetAPI, source, target) + + // Verify data loss was detected + require.Error(t, err, "Expected %s loss to be detected for %s -> %s", lt.name, vp.sourceVersion, vp.targetVersion) + assert.Contains(t, err.Error(), lt.expectedErrMsg) + + // Verify it's a ConversionDataLossError + var dataLossErr *ConversionDataLossError + require.ErrorAs(t, err, &dataLossErr) + }) + } + } +} + +// TestDataLossDetection_NoFalsePositives_AllVersionPermutations verifies that no false positives +// are triggered for any attribute type when counts are equal or increased, across ALL version permutations. +func TestDataLossDetection_NoFalsePositives_AllVersionPermutations(t *testing.T) { + // Helper to create v0/v1 spec with data + createV0V1SpecWithData := func() map[string]interface{} { + return 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": "C"}, + }, + }, + }, + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + map[string]interface{}{"name": "Annotation 2"}, + }, + }, + "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"}, + }, + }, + } + } + + // Helper to create v2alpha1 dashboard with data + createV2alpha1WithData := func() *dashv2alpha1.Dashboard { + return &dashv2alpha1.Dashboard{ + 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{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "B"}}, + }, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "C"}}, + }, + }, + }, + }, + }, + }, + }, + Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{{}, {}}, + Links: []dashv2alpha1.DashboardDashboardLink{{}, {}}, + Variables: []dashv2alpha1.DashboardVariableKind{{}, {}}, + }, + } + } + + // Helper to create v2beta1 dashboard with data + createV2beta1WithData := func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Elements: map[string]dashv2beta1.DashboardElement{ + "panel1": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 1, + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "A"}}, + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "B"}}, + }, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 2, + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + {Spec: dashv2beta1.DashboardPanelQuerySpec{RefId: "C"}}, + }, + }, + }, + }, + }, + }, + }, + Annotations: []dashv2beta1.DashboardAnnotationQueryKind{{}, {}}, + Links: []dashv2beta1.DashboardDashboardLink{{}, {}}, + Variables: []dashv2beta1.DashboardVariableKind{{}, {}}, + }, + } + } + + // All 12 version permutations + versionPairs := []struct { + sourceVersion string + targetVersion string + sourceAPI string + targetAPI string + }{ + // From v0alpha1 + {"v0alpha1", "v1beta1", dashv0.APIVERSION, dashv1.APIVERSION}, + {"v0alpha1", "v2alpha1", dashv0.APIVERSION, dashv2alpha1.APIVERSION}, + {"v0alpha1", "v2beta1", dashv0.APIVERSION, dashv2beta1.APIVERSION}, + // From v1beta1 + {"v1beta1", "v0alpha1", dashv1.APIVERSION, dashv0.APIVERSION}, + {"v1beta1", "v2alpha1", dashv1.APIVERSION, dashv2alpha1.APIVERSION}, + {"v1beta1", "v2beta1", dashv1.APIVERSION, dashv2beta1.APIVERSION}, + // From v2alpha1 + {"v2alpha1", "v0alpha1", dashv2alpha1.APIVERSION, dashv0.APIVERSION}, + {"v2alpha1", "v1beta1", dashv2alpha1.APIVERSION, dashv1.APIVERSION}, + {"v2alpha1", "v2beta1", dashv2alpha1.APIVERSION, dashv2beta1.APIVERSION}, + // From v2beta1 + {"v2beta1", "v0alpha1", dashv2beta1.APIVERSION, dashv0.APIVERSION}, + {"v2beta1", "v1beta1", dashv2beta1.APIVERSION, dashv1.APIVERSION}, + {"v2beta1", "v2alpha1", dashv2beta1.APIVERSION, dashv2alpha1.APIVERSION}, + } + + for _, vp := range versionPairs { + testName := fmt.Sprintf("%s_to_%s_no_loss", vp.sourceVersion, vp.targetVersion) + t.Run(testName, func(t *testing.T) { + var source, target interface{} + + // Create source dashboard with full data + switch vp.sourceVersion { + case "v0alpha1": + source = &dashv0.Dashboard{Spec: dashv0.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v1beta1": + source = &dashv1.Dashboard{Spec: dashv1.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v2alpha1": + source = createV2alpha1WithData() + case "v2beta1": + source = createV2beta1WithData() + } + + // Create target dashboard with same data (no loss) + switch vp.targetVersion { + case "v0alpha1": + target = &dashv0.Dashboard{Spec: dashv0.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v1beta1": + target = &dashv1.Dashboard{Spec: dashv1.DashboardSpec{Object: createV0V1SpecWithData()}} + case "v2alpha1": + target = createV2alpha1WithData() + case "v2beta1": + target = createV2beta1WithData() + } + + // Check for data loss - should be none + err := checkConversionDataLoss(vp.sourceAPI, vp.targetAPI, source, target) + require.NoError(t, err, "Expected no data loss when data is preserved for %s -> %s", vp.sourceVersion, vp.targetVersion) + }) + } } // TestDataLossDetectionOnAllInputFiles tests all conversions from testdata/input @@ -829,7 +1388,7 @@ func TestDataLossDetectionOnAllInputFiles(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := testutil.NewDataSourceProvider(testutil.StandardTestConfig) leProvider := testutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -1359,3 +1918,1148 @@ func extractQueryInfoV2beta1(spec dashv2beta1.DashboardSpec) []queryInfo { return queries } + +// TestDataLossDetection_IntegrationWithRealConversions performs actual conversions +// using the scheme and verifies that data loss detection works correctly. +func TestDataLossDetection_IntegrationWithRealConversions(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := testutil.NewDataSourceProvider(testutil.StandardTestConfig) + leProvider := testutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + // + // ========== V0/V1 -> V2 CONVERSIONS (These should all pass) ========== + // + + // Test 1: V0 Collapsed Rows -> V2alpha1 + t.Run("Integration_V0CollapsedRows_To_V2alpha1", func(t *testing.T) { + source := &dashv0.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashv0.APIVERSION, + Kind: "Dashboard", + }, + Spec: dashv0.DashboardSpec{ + Object: createV0V1WithCollapsedRows(2, 2, 1), + }, + } + sourceStats := collectStatsV0V1(source.Spec.Object) + + target := &dashv2alpha1.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV2alpha1(target.Spec) + + t.Logf("V0 Collapsed Rows -> V2alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 2: V0 Expanded Rows -> V2alpha1 + t.Run("Integration_V0ExpandedRows_To_V2alpha1", func(t *testing.T) { + source := &dashv0.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashv0.APIVERSION, + Kind: "Dashboard", + }, + Spec: dashv0.DashboardSpec{ + Object: createV0V1WithExpandedRows(2, 2, 1), + }, + } + sourceStats := collectStatsV0V1(source.Spec.Object) + + target := &dashv2alpha1.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV2alpha1(target.Spec) + + t.Logf("V0 Expanded Rows -> V2alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 3: V0 Flat Panels -> V2alpha1 + t.Run("Integration_V0FlatPanels_To_V2alpha1", func(t *testing.T) { + source := &dashv0.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashv0.APIVERSION, + Kind: "Dashboard", + }, + Spec: dashv0.DashboardSpec{ + Object: createV0V1FlatPanels(4, 1), + }, + } + sourceStats := collectStatsV0V1(source.Spec.Object) + + target := &dashv2alpha1.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV2alpha1(target.Spec) + + t.Logf("V0 Flat Panels -> V2alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 4: V0 Mixed Layout -> V2alpha1 + t.Run("Integration_V0MixedLayout_To_V2alpha1", func(t *testing.T) { + source := &dashv0.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashv0.APIVERSION, + Kind: "Dashboard", + }, + Spec: dashv0.DashboardSpec{ + Object: createV0V1MixedAllTypes(), + }, + } + sourceStats := collectStatsV0V1(source.Spec.Object) + + target := &dashv2alpha1.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV2alpha1(target.Spec) + + t.Logf("V0 Mixed Layout -> V2alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // + // ========== V2 -> V0/V1 CONVERSIONS ========== + // These tests document the current state of V2 -> V0 conversions. + // + + // Test 5: V2 Simple Tabs -> V0alpha1 + t.Run("Integration_V2SimpleTabs_To_V0alpha1", func(t *testing.T) { + source := createV2DashboardWithSimpleTabs() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Simple Tabs -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 6: V2 Rows Collapsed -> V0alpha1 + t.Run("Integration_V2RowsCollapsed_To_V0alpha1", func(t *testing.T) { + source := createV2DashboardWithRowsCollapsed() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Rows Collapsed -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 7: V2 Rows Expanded -> V0alpha1 + t.Run("Integration_V2RowsExpanded_To_V0alpha1", func(t *testing.T) { + source := createV2DashboardWithRowsExpanded() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Rows Expanded -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 8: V2 Tabs with Nested Rows -> V0alpha1 + t.Run("Integration_V2TabsWithNestedRows_To_V0alpha1", func(t *testing.T) { + source := createV2DashboardWithTabsAndNestedRows() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Tabs with Nested Rows -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 9: V2 Rows with Nested Tabs -> V0alpha1 + t.Run("Integration_V2RowsWithNestedTabs_To_V0alpha1", func(t *testing.T) { + source := createV2DashboardWithRowsAndNestedTabs() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Rows with Nested Tabs -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // Test 10: V2 Complex Nesting -> V0alpha1 + t.Run("Integration_V2ComplexNesting_To_V0alpha1", func(t *testing.T) { + source := createV2ComplexNestedDashboard() + source.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + sourceStats := collectStatsV2alpha1(source.Spec) + + target := &dashv0.Dashboard{} + err := scheme.Convert(source, target, nil) + require.NoError(t, err, "Conversion should succeed") + + targetStats := collectStatsV0V1(target.Spec.Object) + + t.Logf("V2 Complex Nesting -> V0alpha1:") + t.Logf(" Source: panels=%d, queries=%d", sourceStats.panelCount, sourceStats.queryCount) + t.Logf(" Target: panels=%d, queries=%d", targetStats.panelCount, targetStats.queryCount) + + assert.Equal(t, sourceStats.panelCount, targetStats.panelCount, "Panel count should match") + assert.Equal(t, sourceStats.queryCount, targetStats.queryCount, "Query count should match") + }) + + // + // ========== ROUND-TRIP CONVERSIONS ========== + // + + // Test 11: V2 -> V0 -> V2 round-trip (panel count should be preserved) + t.Run("Integration_RoundTrip_V2_V0_V2", func(t *testing.T) { + original := createV2DashboardWithSimpleTabs() + original.TypeMeta = metav1.TypeMeta{ + APIVersion: dashv2alpha1.APIVERSION, + Kind: "Dashboard", + } + originalStats := collectStatsV2alpha1(original.Spec) + + // V2 -> V0 + intermediate := &dashv0.Dashboard{} + err := scheme.Convert(original, intermediate, nil) + require.NoError(t, err, "V2 -> V0 conversion should succeed") + intermediateStats := collectStatsV0V1(intermediate.Spec.Object) + + // V0 -> V2 + final := &dashv2alpha1.Dashboard{} + err = scheme.Convert(intermediate, final, nil) + require.NoError(t, err, "V0 -> V2 conversion should succeed") + finalStats := collectStatsV2alpha1(final.Spec) + + t.Logf("Round-trip V2 -> V0 -> V2:") + t.Logf(" Original (V2): panels=%d, queries=%d", originalStats.panelCount, originalStats.queryCount) + t.Logf(" Intermediate (V0): panels=%d, queries=%d", intermediateStats.panelCount, intermediateStats.queryCount) + t.Logf(" Final (V2): panels=%d, queries=%d", finalStats.panelCount, finalStats.queryCount) + + // Panel count should be preserved through round-trip + assert.Equal(t, originalStats.panelCount, finalStats.panelCount, "Panel count should match after round-trip") + + // Document query behavior + if intermediateStats.queryCount < originalStats.queryCount { + t.Logf(" ⚠️ Queries lost in V2->V0: %d -> %d", originalStats.queryCount, intermediateStats.queryCount) + } + if finalStats.queryCount != originalStats.queryCount { + t.Logf(" ⚠️ Final query count differs: original=%d, final=%d", originalStats.queryCount, finalStats.queryCount) + } + }) + + // Test 12: V0 -> V2 -> V0 round-trip + t.Run("Integration_RoundTrip_V0_V2_V0", func(t *testing.T) { + original := &dashv0.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashv0.APIVERSION, + Kind: "Dashboard", + }, + Spec: dashv0.DashboardSpec{ + Object: createV0V1MixedAllTypes(), + }, + } + originalStats := collectStatsV0V1(original.Spec.Object) + + // V0 -> V2 + intermediate := &dashv2alpha1.Dashboard{} + err := scheme.Convert(original, intermediate, nil) + require.NoError(t, err, "V0 -> V2 conversion should succeed") + intermediateStats := collectStatsV2alpha1(intermediate.Spec) + + // V2 -> V0 + final := &dashv0.Dashboard{} + err = scheme.Convert(intermediate, final, nil) + require.NoError(t, err, "V2 -> V0 conversion should succeed") + finalStats := collectStatsV0V1(final.Spec.Object) + + t.Logf("Round-trip V0 -> V2 -> V0:") + t.Logf(" Original (V0): panels=%d, queries=%d", originalStats.panelCount, originalStats.queryCount) + t.Logf(" Intermediate (V2): panels=%d, queries=%d", intermediateStats.panelCount, intermediateStats.queryCount) + t.Logf(" Final (V0): panels=%d, queries=%d", finalStats.panelCount, finalStats.queryCount) + + // V0 -> V2 should preserve everything + assert.Equal(t, originalStats.panelCount, intermediateStats.panelCount, "V0->V2 should preserve panels") + assert.Equal(t, originalStats.queryCount, intermediateStats.queryCount, "V0->V2 should preserve queries") + + // Document what happens in V2 -> V0 + assert.Equal(t, originalStats.panelCount, finalStats.panelCount, "Panel count should match after round-trip") + + if finalStats.queryCount < originalStats.queryCount { + t.Logf(" ⚠️ Queries lost in V2->V0 leg: %d -> %d", intermediateStats.queryCount, finalStats.queryCount) + } + }) +} + +// Helper functions to create test dashboards + +// ========== V2 Dashboard Fixtures ========== + +// createV2DashboardWithSimpleTabs creates a V2 dashboard with simple TabsLayout +// Structure: TabsLayout > Tab1[panel1, panel2] + Tab2[panel3, panel4] +func createV2DashboardWithSimpleTabs() *dashv2alpha1.Dashboard { + elements := map[string]dashv2alpha1.DashboardElement{} + for i := 1; i <= 4; i++ { + tabNum := (i-1)/2 + 1 + elements[fmt.Sprintf("panel%d", i)] = dashv2alpha1.DashboardElement{ + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: float64(i), + Title: fmt.Sprintf("Panel %d - Tab %d", i, tabNum), + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + }, + }, + }, + }, + }, + } + } + + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + TabsLayoutKind: &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: dashv2alpha1.DashboardTabsLayoutSpec{ + Tabs: []dashv2alpha1.DashboardTabsLayoutTabKind{ + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 2"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// createV2DashboardWithRowsCollapsed creates a V2 dashboard with RowsLayout (collapsed rows) +// Structure: RowsLayout > Row1(collapsed)[panel1, panel2] + Row2(collapsed)[panel3, panel4] +func createV2DashboardWithRowsCollapsed() *dashv2alpha1.Dashboard { + elements := map[string]dashv2alpha1.DashboardElement{} + for i := 1; i <= 4; i++ { + rowNum := (i-1)/2 + 1 + elements[fmt.Sprintf("panel%d", i)] = dashv2alpha1.DashboardElement{ + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: float64(i), + Title: fmt.Sprintf("Panel %d - Row %d (collapsed)", i, rowNum), + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + }, + }, + }, + }, + }, + } + } + + collapsed := true + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1 (collapsed)"), + Collapse: &collapsed, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 2 (collapsed)"), + Collapse: &collapsed, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// createV2DashboardWithRowsExpanded creates a V2 dashboard with RowsLayout (expanded rows) +// Structure: RowsLayout > Row1(expanded)[panel1, panel2] + Row2(expanded)[panel3, panel4] +func createV2DashboardWithRowsExpanded() *dashv2alpha1.Dashboard { + elements := map[string]dashv2alpha1.DashboardElement{} + for i := 1; i <= 4; i++ { + rowNum := (i-1)/2 + 1 + elements[fmt.Sprintf("panel%d", i)] = dashv2alpha1.DashboardElement{ + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: float64(i), + Title: fmt.Sprintf("Panel %d - Row %d (expanded)", i, rowNum), + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}, + }, + }, + }, + }, + }, + } + } + + expanded := false // collapse=false means expanded + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1 (expanded)"), + Collapse: &expanded, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 2 (expanded)"), + Collapse: &expanded, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func createV0V1FlatPanels(numPanels, queriesPerPanel int) map[string]interface{} { + panels := make([]interface{}, numPanels) + for i := 0; i < numPanels; i++ { + targets := make([]interface{}, queriesPerPanel) + for q := 0; q < queriesPerPanel; q++ { + targets[q] = map[string]interface{}{"refId": fmt.Sprintf("%c", 'A'+q)} + } + panels[i] = map[string]interface{}{ + "id": float64(i + 1), + "title": fmt.Sprintf("Panel %d", i+1), + "type": "timeseries", + "targets": targets, + } + } + return map[string]interface{}{"panels": panels} +} + +func createV0V1WithCollapsedRows(numRows, panelsPerRow, queriesPerPanel int) map[string]interface{} { + panels := make([]interface{}, 0) + panelID := 1 + + for r := 0; r < numRows; r++ { + // Create collapsed panels for this row + collapsedPanels := make([]interface{}, panelsPerRow) + for p := 0; p < panelsPerRow; p++ { + targets := make([]interface{}, queriesPerPanel) + for q := 0; q < queriesPerPanel; q++ { + targets[q] = map[string]interface{}{"refId": fmt.Sprintf("%c", 'A'+q)} + } + collapsedPanels[p] = map[string]interface{}{ + "id": float64(panelID), + "title": fmt.Sprintf("Panel %d (in Row %d)", panelID, r+1), + "type": "timeseries", + "targets": targets, + } + panelID++ + } + + // Add row with collapsed panels + panels = append(panels, map[string]interface{}{ + "id": float64(100 + r), + "title": fmt.Sprintf("Row %d (collapsed)", r+1), + "type": "row", + "collapsed": true, + "panels": collapsedPanels, + }) + } + return map[string]interface{}{"panels": panels} +} + +func createV0V1WithExpandedRows(numRows, panelsPerRow, queriesPerPanel int) map[string]interface{} { + panels := make([]interface{}, 0) + panelID := 1 + + for r := 0; r < numRows; r++ { + // Add expanded row (no nested panels) + panels = append(panels, map[string]interface{}{ + "id": float64(100 + r), + "title": fmt.Sprintf("Row %d (expanded)", r+1), + "type": "row", + // No "panels" field when row is expanded + }) + + // Add panels after the row + for p := 0; p < panelsPerRow; p++ { + targets := make([]interface{}, queriesPerPanel) + for q := 0; q < queriesPerPanel; q++ { + targets[q] = map[string]interface{}{"refId": fmt.Sprintf("%c", 'A'+q)} + } + panels = append(panels, map[string]interface{}{ + "id": float64(panelID), + "title": fmt.Sprintf("Panel %d (after Row %d)", panelID, r+1), + "type": "timeseries", + "targets": targets, + }) + panelID++ + } + } + return map[string]interface{}{"panels": panels} +} + +func createV2DashboardWithTabsAndNestedRows() *dashv2alpha1.Dashboard { + // 2 tabs, each with 1 row containing 2 panels = 4 panels total + // Structure: TabsLayout > Tab1[RowsLayout > Row[panel1, panel2]] + Tab2[RowsLayout > Row[panel3, panel4]] + elements := map[string]dashv2alpha1.DashboardElement{ + "panel1": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 1, Title: "Panel 1 - Tab1/Row1", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, Title: "Panel 2 - Tab1/Row1", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "stat"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel3": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 3, Title: "Panel 3 - Tab2/Row1", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "gauge"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel4": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 4, Title: "Panel 4 - Tab2/Row1", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "table"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + } + + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + TabsLayoutKind: &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: dashv2alpha1.DashboardTabsLayoutSpec{ + Tabs: []dashv2alpha1.DashboardTabsLayoutTabKind{ + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 2"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func createV2DashboardWithRowsAndNestedTabs() *dashv2alpha1.Dashboard { + // 2 rows, each containing a tab layout with 2 panels = 4 panels total + // Structure: RowsLayout > Row1[TabsLayout > Tab[panel1, panel2]] + Row2[TabsLayout > Tab[panel3, panel4]] + elements := map[string]dashv2alpha1.DashboardElement{ + "panel1": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 1, Title: "Panel 1 - Row1/TabA", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, Title: "Panel 2 - Row1/TabB", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "stat"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel3": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 3, Title: "Panel 3 - Row2/TabA", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "gauge"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + "panel4": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 4, Title: "Panel 4 - Row2/TabB", + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "table"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + }, + } + + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + TabsLayoutKind: &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: dashv2alpha1.DashboardTabsLayoutSpec{ + Tabs: []dashv2alpha1.DashboardTabsLayoutTabKind{ + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab A"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 2"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + TabsLayoutKind: &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: dashv2alpha1.DashboardTabsLayoutSpec{ + Tabs: []dashv2alpha1.DashboardTabsLayoutTabKind{ + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab A"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func createV2ComplexNestedDashboard() *dashv2alpha1.Dashboard { + // Complex: 2 tabs, Tab1 has 2 rows (each with 2 panels), Tab2 has 2 panels directly + // Total: 6 panels + // Structure: TabsLayout > Tab1[RowsLayout > Row1[p1,p2] + Row2[p3,p4]] + Tab2[GridLayout > p5,p6] + elements := map[string]dashv2alpha1.DashboardElement{} + for i := 1; i <= 6; i++ { + elements[fmt.Sprintf("panel%d", i)] = dashv2alpha1.DashboardElement{ + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: float64(i), + Title: fmt.Sprintf("Panel %d", i), + VizConfig: dashv2alpha1.DashboardVizConfigKind{Kind: "timeseries"}, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{{Spec: dashv2alpha1.DashboardPanelQuerySpec{RefId: "A"}}}, + }, + }, + }, + }, + } + } + + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Elements: elements, + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + TabsLayoutKind: &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: dashv2alpha1.DashboardTabsLayoutSpec{ + Tabs: []dashv2alpha1.DashboardTabsLayoutTabKind{ + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: []dashv2alpha1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 1"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel1"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel2"}}}, + }, + }, + }, + }, + }, + }, + { + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: ptrString("Row 2"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel3"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel4"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Kind: "TabsLayoutTab", + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: ptrString("Tab 2"), + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{ + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel5"}}}, + {Spec: dashv2alpha1.DashboardGridLayoutItemSpec{Element: dashv2alpha1.DashboardElementReference{Name: "panel6"}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// createV0V1MixedAllTypes creates a V0/V1 dashboard with ALL layout types: +// - Panels out of rows (flat, top-level) +// - Collapsed row with nested panels +// - Expanded row followed by panels +// Structure: [p1, p2, Row1(collapsed){p3,p4}, Row2(expanded), p5, p6] +// Total: 6 panels +func createV0V1MixedAllTypes() map[string]interface{} { + panels := []interface{}{ + // Flat panels out of rows (at the beginning) + map[string]interface{}{ + "id": float64(1), + "title": "Panel 1 (flat, no row)", + "type": "timeseries", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "title": "Panel 2 (flat, no row)", + "type": "stat", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + + // Collapsed row with nested panels + map[string]interface{}{ + "id": float64(100), + "title": "Row 1 (collapsed)", + "type": "row", + "collapsed": true, + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(3), + "title": "Panel 3 (in collapsed row)", + "type": "gauge", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(4), + "title": "Panel 4 (in collapsed row)", + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + }, + }, + + // Expanded row (empty, panels follow after) + map[string]interface{}{ + "id": float64(101), + "title": "Row 2 (expanded)", + "type": "row", + // No "panels" field when row is expanded + }, + + // Panels after expanded row + map[string]interface{}{ + "id": float64(5), + "title": "Panel 5 (after expanded row)", + "type": "bargauge", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(6), + "title": "Panel 6 (after expanded row)", + "type": "piechart", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + } + + return map[string]interface{}{"panels": panels} +} + +// ptrString is a helper to create a pointer to a string +func ptrString(s string) *string { + return &s +} diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index 88dfb683225..63338e7ea70 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -35,7 +35,7 @@ func TestConversionMatrixExist(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) versions := []metav1.Object{ &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, @@ -89,7 +89,7 @@ func TestDashboardConversionToAllVersions(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -309,7 +309,7 @@ func TestMigratedDashboardsConversion(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -428,7 +428,7 @@ func setupTestConversionScheme(t *testing.T) *runtime.Scheme { t.Helper() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) scheme := runtime.NewScheme() err := RegisterConversions(scheme, dsProvider, leProvider) @@ -527,7 +527,7 @@ func TestConversionMetrics(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -694,7 +694,7 @@ func TestConversionMetricsWrapper(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -864,7 +864,7 @@ func TestSchemaVersionExtraction(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -910,7 +910,7 @@ func TestConversionLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -1003,7 +1003,7 @@ func TestConversionLogLevels(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("log levels and structured fields verification", func(t *testing.T) { // Create test wrapper to verify logging behavior @@ -1076,7 +1076,7 @@ func TestConversionLoggingFields(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) 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 5b2700bce60..9cbdec193e8 100644 --- a/apps/dashboard/pkg/migration/conversion/metrics.go +++ b/apps/dashboard/pkg/migration/conversion/metrics.go @@ -17,7 +17,9 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -var logger = logging.DefaultLogger.With("logger", "dashboard.conversion") +func getLogger() logging.Logger { + return logging.DefaultLogger.With("logger", "dashboard.conversion") +} // getErroredSchemaVersionFunc determines the schema version function that errored func getErroredSchemaVersionFunc(err error) string { @@ -83,20 +85,20 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion // Only track schema versions for v0/v1 dashboards (v2+ info is redundant with API version) switch source := a.(type) { case *dashv0.Dashboard: - dashboardUID = string(source.UID) + dashboardUID = source.Name if source.Spec.Object != nil { sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object) } case *dashv1.Dashboard: - dashboardUID = string(source.UID) + dashboardUID = source.Name if source.Spec.Object != nil { sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object) } case *dashv2alpha1.Dashboard: - dashboardUID = string(source.UID) + dashboardUID = source.Name // Don't track schema version for v2+ (redundant with API version) case *dashv2beta1.Dashboard: - dashboardUID = string(source.UID) + dashboardUID = source.Name // Don't track schema version for v2+ (redundant with API version) } @@ -197,9 +199,9 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ) if errorType == "schema_minimum_version_error" { - logger.Warn("Dashboard conversion failed", logFields...) + getLogger().Warn("Dashboard conversion failed", logFields...) } else { - logger.Error("Dashboard conversion failed", logFields...) + getLogger().Error("Dashboard conversion failed", logFields...) } } else { // Record success metrics @@ -235,7 +237,7 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ) } - logger.Debug("Dashboard conversion succeeded", successLogFields...) + getLogger().Debug("Dashboard conversion succeeded", successLogFields...) } return nil diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json index 89ce80876d2..e4e6f8831da 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json @@ -300,15 +300,9 @@ "y": 0 }, "id": 6, - "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" - }, + "options": {}, + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown", "pluginVersion": "11.0.0-pre", "targets": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 863d7b2a102..62648d7a4aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -71,14 +71,13 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -150,14 +149,13 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -229,14 +227,13 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -271,85 +268,6 @@ "title": "Center and bar glow", "type": "radialbar" }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 12, - "y": 1 - }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, { "datasource": { "type": "grafana-testdata-datasource" @@ -391,10 +309,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -470,10 +387,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -549,10 +465,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -641,10 +556,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -720,10 +634,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -799,10 +712,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -878,10 +790,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -974,10 +885,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1053,10 +963,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1132,10 +1041,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1211,10 +1119,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1290,10 +1197,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1386,10 +1292,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1469,10 +1374,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1552,10 +1456,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1641,14 +1544,14 @@ "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1662,8 +1565,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1730,11 +1632,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1748,8 +1649,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1830,11 +1730,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1848,8 +1747,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1917,12 +1815,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1934,10 +1829,10 @@ "segmentCount": 12, "segmentSpacing": 0.3, "shape": "circle", + "barShape": "rounded", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2004,11 +1899,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2022,8 +1916,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2090,11 +1983,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2108,8 +2000,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2147,4 +2038,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json index d4dd7980100..dda0fbcd432 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json @@ -955,10 +955,8 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1162,4 +1160,4 @@ "title": "Panel tests - Old gauge to new", "uid": "panel-tests-old-gauge-to-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.angular-migrations.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.angular-migrations.json new file mode 100644 index 00000000000..3d496f51731 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.angular-migrations.json @@ -0,0 +1,1310 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "angular-migrations-test" + }, + "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": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate graph panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate graph panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate table (old) panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate table (old) panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate piechart panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate piechart panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate worldmap panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate worldmap panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate stat panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate stat panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate (TRUE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate (FALSE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Disable angular (TRUE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.disableAngular=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Disable angular (FALSE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.disableAngular=false" + } + ], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "type": "graph", + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 6, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 28, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.260951647569786,\n 1.887442338051216,\n 2.1526144400893514,\n 1.7287721375237766,\n 1.7262902137793208\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod1\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 1.589539045095202,\n 1.5914283506847613,\n 1.8976990616650726,\n 1.758223085999124,\n 2.2294649594813816\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod2\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.0914263380328766,\n 1.8164545521094575,\n 1.621111084665713,\n 1.3902653996444705,\n 1.482803315949775\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "barchart", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 11 + }, + "id": 29, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar chart panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "default": false, + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 32, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.3.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 28, + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode (with legend calcs)", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "bargauge", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis histogram mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "histogram", + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 33, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar gauge panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 31, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Histogram panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "table-old", + "columns": [], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 33 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk_table" + } + ], + "title": "Table (old)", + "transform": "table", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 33 + }, + "id": 7, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Table (old) \u003e\u003e Table\n\nKnown issues:\n* wrapping text\n* style changes", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-singlestat-panel", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 43 + }, + "id": 9, + "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": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "thresholds": "", + "title": "grafana-singlestat-panel", + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "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": 23, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 43 + }, + "id": 10, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Singlestat \u003e\u003e Stat\n\nKnown issues:\n* limited options", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-piechart-panel", + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 51 + }, + "id": 24, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk_table" + } + ], + "title": "grafana-piechart-panel", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 51 + }, + "id": 25, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-piechart-panel \u003e\u003e piechart\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-worldmap-panel", + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "decimals": 0, + "esMetric": "Count", + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 61 + }, + "hideEmpty": false, + "hideZero": false, + "id": 26, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "maxDataPoints": 1, + "mouseWheelZoom": false, + "options": { + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "pluginVersion": "10.4.0-pre", + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "targets": [ + { + "csvFileName": "flight_info_by_state.csv", + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_file" + } + ], + "thresholds": "0,10", + "title": "grafana-worldmap-panel", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + }, + { + "id": "reduce", + "options": { + "reducers": [ + "sum" + ] + } + } + ], + "type": "geomap", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 61 + }, + "id": 27, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-worldmap-panel \u003e\u003e geomap\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [ + "gdev", + "migrations", + "angular" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Devenv - Panel migrations", + "uid": "cdd412c4", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.bom-in-links.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.bom-in-links.json new file mode 100644 index 00000000000..86992c3380c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.bom-in-links.json @@ -0,0 +1,142 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "bom-in-links-test", + "namespace": "org-1", + "labels": { + "test": "bom-stripping" + } + }, + "spec": { + "title": "BOM Stripping Test Dashboard", + "description": "Testing that BOM characters are stripped from URLs during conversion", + "schemaVersion": 42, + "tags": ["test", "bom"], + "editable": true, + "links": [ + { + "title": "Dashboard link with BOM", + "type": "link", + "url": "http://example.com?var=${datasource}&other=value", + "targetBlank": true, + "icon": "external link" + } + ], + "panels": [ + { + "id": 1, + "type": "table", + "title": "Panel with BOM in field config override links", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green"}, + {"color": "red", "value": 80} + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "server" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Override link with BOM", + "url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}&var-server=${__value.raw}" + } + ] + } + ] + } + ] + }, + "links": [ + { + "title": "Panel data link with BOM", + "url": "http://example.com/${__data.fields.cluster}&var=value", + "targetBlank": true + } + ], + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "test-ds" + } + } + ] + }, + { + "id": 2, + "type": "timeseries", + "title": "Panel with BOM in options dataLinks", + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "options": { + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom" + }, + "dataLinks": [ + { + "title": "Options data link with BOM", + "url": "http://example.com?series=${__series.name}&time=${__value.time}", + "targetBlank": true + } + ] + }, + "fieldConfig": { + "defaults": { + "links": [ + { + "title": "Field config default link with BOM", + "url": "http://example.com?field=${__field.name}&value=${__value.raw}", + "targetBlank": false + } + ] + }, + "overrides": [] + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "test-ds" + } + } + ] + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m"] + } + } +} + diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.groupby.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.groupby.json new file mode 100644 index 00000000000..88527ef6036 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.groupby.json @@ -0,0 +1,166 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "groupby-test" + }, + "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": { + "type": "prometheus", + "uid": "test-uid" + }, + "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": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "editorMode": "code", + "expr": "sum(counters_requests)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "works with group by var", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ], + "value": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "name": "Group by", + "type": "groupby" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "groupby test", + "weekStart": "" + } + } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-target-missing-refid.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-target-missing-refid.json new file mode 100644 index 00000000000..65bf0b136a6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-target-missing-refid.json @@ -0,0 +1,156 @@ +{ + "apiVersion": "dashboard.grafana.app/v1beta1", + "kind": "Dashboard", + "metadata": { + "name": "ad5vfcn", + "namespace": "default", + "uid": "dlMZZl6GndU8gJLUQSmgZxXBPCNXyXhNBeQJhHXl0r4X", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-11-28T10:14:21Z", + "labels": { + "grafana.app/deprecatedInternalID": "288" + }, + "annotations": { + "grafana.app/createdBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedTimestamp": "2025-11-28T10:15:06Z" + } + }, + "spec": { + "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, + "id": 288, + "links": [], + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "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, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": {}, + "queryType": "randomWalk" + }, + { + "datasource": {}, + "queryType": "randomWalk" + } + ], + "title": "New panel", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Panel ds inheritance ", + "uid": "ad5vfcn", + "version": 2 + }, + "status": {} +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.value-mapping-and-overrides.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.value-mapping-and-overrides.json new file mode 100644 index 00000000000..c0a82877ecc --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.value-mapping-and-overrides.json @@ -0,0 +1,603 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + }, + "annotations": {}, + "managedFields": [] + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "critical": { + "color": "red", + "index": 0, + "text": "Critical!" + }, + "warning": { + "color": "orange", + "index": 1, + "text": "Warning" + }, + "ok": { + "color": "green", + "index": 2, + "text": "OK" + } + }, + "type": "value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "targets": [ + { + "expr": "up", + "refId": "A" + } + ], + "title": "ValueMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "from": 0, + "to": 50, + "result": { + "color": "green", + "index": 0, + "text": "Low" + } + }, + "type": "range" + }, + { + "options": { + "from": 50, + "to": 80, + "result": { + "color": "orange", + "index": 1, + "text": "Medium" + } + }, + "type": "range" + }, + { + "options": { + "from": 80, + "to": 100, + "result": { + "color": "red", + "index": 2, + "text": "High" + } + }, + "type": "range" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "targets": [ + { + "expr": "cpu_usage_percent", + "refId": "A" + } + ], + "title": "RangeMap Example", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "pattern": "/^error.*/", + "result": { + "color": "red", + "index": 0, + "text": "Error" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^warn.*/", + "result": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^info.*/", + "result": { + "color": "blue", + "index": 2, + "text": "Info" + } + }, + "type": "regex" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "targets": [ + { + "expr": "log_level", + "refId": "A" + } + ], + "title": "RegexMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 0, + "text": "No Data" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "gray", + "index": 1, + "text": "Not a Number" + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "gray", + "index": 2, + "text": "N/A" + } + }, + "type": "special" + }, + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 3, + "text": "Yes" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 4, + "text": "No" + } + }, + "type": "special" + }, + { + "options": { + "match": "empty", + "result": { + "color": "gray", + "index": 5, + "text": "Empty" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "targets": [ + { + "expr": "some_metric", + "refId": "A" + } + ], + "title": "SpecialValueMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "success": { + "color": "green", + "index": 0, + "text": "Success" + }, + "failure": { + "color": "red", + "index": 1, + "text": "Failure" + } + }, + "type": "value" + }, + { + "options": { + "from": 0, + "to": 100, + "result": { + "color": "blue", + "index": 2, + "text": "In Range" + } + }, + "type": "range" + }, + { + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "color": "purple", + "index": 3, + "text": "ID Format" + } + }, + "type": "regex" + }, + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 4, + "text": "Missing" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "reducer": "allIsNull", + "op": "gte", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 5, + "targets": [ + { + "expr": "combined_metric", + "refId": "A" + }, + { + "expr": "secondary_metric", + "refId": "B" + } + ], + "title": "Combined Mappings and Overrides Example", + "type": "table" + } + ], + "schemaVersion": 42, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Value Mapping and Overrides Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + }, + "access": { + "slug": "value-mapping-test", + "url": "/d/value-mapping-test/value-mapping-and-overrides-test", + "canSave": true, + "canEdit": true, + "canAdmin": true, + "canStar": true, + "canDelete": true, + "annotationsPermissions": { + "dashboard": { + "canAdd": true, + "canEdit": true, + "canDelete": true + }, + "organization": { + "canAdd": true, + "canEdit": true, + "canDelete": true + } + } + } +} 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 index ae1ef7cd04e..d6010e5100e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -42,7 +42,7 @@ "regex": "", "skipUrlSync": false, "refresh": 1 - }, + }, { "name": "query_var", "type": "query", @@ -81,6 +81,7 @@ "allValue": ".*", "multi": true, "regex": "/.*9090.*/", + "regexApplyTo": "text", "skipUrlSync": false, "refresh": 2, "sort": 1, @@ -107,7 +108,7 @@ }, { "selected": false, - "text": "staging", + "text": "staging", "value": "staging" }, { @@ -335,6 +336,7 @@ "allValue": "*", "multi": true, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "skipUrlSync": false, "refresh": 1, "sort": 2, @@ -354,4 +356,4 @@ }, "links": [] } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.tab-with-multiple-panels.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.tab-with-multiple-panels.json new file mode 100644 index 00000000000..c2bb49cc5cb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.tab-with-multiple-panels.json @@ -0,0 +1,715 @@ +{ +"kind": "DashboardWithAccessInfo", +"apiVersion": "dashboard.grafana.app/v2beta1", +"metadata": { + "name": "adt885j", + "namespace": "default", + "uid": "yTWet6JgBjlRIWnqRE9ZOmUycfT0tEkr2mljaln1GWIX", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-12-16T10:44:31Z", + "labels": { + "grafana.app/deprecatedInternalID": "2409" + }, + "annotations": { + "grafana.app/createdBy": "user:u000000001", + "grafana.app/updatedBy": "user:u000000001", + "grafana.app/updatedTimestamp": "2025-12-16T10:51:14Z" + } +}, +"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 + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel1", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel2", + "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": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel3", + "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": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel4", + "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": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel5", + "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": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "TabsLayout", + "spec": { + "tabs": [ + { + "kind": "TabsLayoutTab", + "spec": { + "title": "Tab1", + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 7, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 7, + "y": 0, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 15, + "y": 0, + "width": 9, + "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" + } + } + } + ] + } + } + } + } + ] + } + }, + "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 tabs", + "variables": [] +}, +"status": {}, +"access": { + "slug": "dashboard-with-tabs", + "url": "/d/adt885j/dashboard-with-tabs", + "isPublic": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "canStar": true, + "canDelete": true, + "annotationsPermissions": { + "dashboard": { + "canAdd": true, + "canEdit": true, + "canDelete": true + }, + "organization": { + "canAdd": true, + "canEdit": true, + "canDelete": true + } + } +} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.value-mapping-and-overrides.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.value-mapping-and-overrides.json new file mode 100644 index 00000000000..54507dafbea --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.value-mapping-and-overrides.json @@ -0,0 +1,864 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "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": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "ValueMap Example", + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "up" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "critical": { + "text": "Critical!", + "color": "red", + "index": 0 + }, + "ok": { + "text": "OK", + "color": "green", + "index": 2 + }, + "warning": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "RangeMap Example", + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "cpu_usage_percent" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "range", + "options": { + "from": 0, + "to": 50, + "result": { + "text": "Low", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 80, + "result": { + "text": "Medium", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": 80, + "to": 100, + "result": { + "text": "High", + "color": "red", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "RegexMap Example", + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "log_level" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "/^error.*/", + "result": { + "text": "Error", + "color": "red", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^warn.*/", + "result": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^info.*/", + "result": { + "text": "Info", + "color": "blue", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "SpecialValueMap Example", + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "some_metric" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "No Data", + "color": "gray", + "index": 0 + } + } + }, + { + "type": "special", + "options": { + "match": "nan", + "result": { + "text": "Not a Number", + "color": "gray", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "null+nan", + "result": { + "text": "N/A", + "color": "gray", + "index": 2 + } + } + }, + { + "type": "special", + "options": { + "match": "true", + "result": { + "text": "Yes", + "color": "green", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "false", + "result": { + "text": "No", + "color": "red", + "index": 4 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": "Empty", + "color": "gray", + "index": 5 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Empty Properties Override Example", + "description": "Panel with override that has empty properties array - tests conversion of overrides without any property modifications", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "empty_override_metric" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "field_with_empty_override" + }, + "properties": [] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Combined Mappings and Overrides Example", + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "combined_metric" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "secondary_metric" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "failure": { + "text": "Failure", + "color": "red", + "index": 1 + }, + "success": { + "text": "Success", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 100, + "result": { + "text": "In Range", + "color": "blue", + "index": 2 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "text": "ID Format", + "color": "purple", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Missing", + "color": "gray", + "index": 4 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": 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": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 24, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "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": "Value Mapping and Overrides Test", + "variables": [] + }, + "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 index 7a4a4d2c4b7..669e2e742a6 100644 --- 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 @@ -66,7 +66,29 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "thresholds": [ + "20", + "30" + ] + }, + { + "align": "auto", + "thresholds": [ + "200", + "300" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +132,22 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "thresholds": [ + "50", + "75" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +191,22 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "styles": [ + { + "thresholds": [ + "5", + "10", + "15" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -237,5 +289,10 @@ "title": "V10 Table Thresholds Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2beta1.json index 69d75b09dcf..22a36b375a0 100644 --- 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 @@ -69,7 +69,29 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "thresholds": [ + "20", + "30" + ] + }, + { + "align": "auto", + "thresholds": [ + "200", + "300" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +137,22 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "thresholds": [ + "50", + "75" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +198,22 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "styles": [ + { + "thresholds": [ + "5", + "10", + "15" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -244,5 +296,10 @@ "title": "V10 Table Thresholds Test", "variables": [] }, - "status": {} + "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 index f427b5dbcb7..b9007aeab43 100644 --- 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 @@ -66,7 +66,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +121,20 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -206,5 +230,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json index 8e59bbd21ca..75255794c5c 100644 --- 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 @@ -69,7 +69,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +126,20 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -213,5 +237,10 @@ } ] }, - "status": {} + "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 index f55769f82b2..5e69664e182 100644 --- 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 @@ -203,5 +203,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json index febee713d97..e3d0bff265d 100644 --- 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 @@ -216,5 +216,10 @@ } ] }, - "status": {} + "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 index a1ef92d3d50..091b4103636 100644 --- 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 @@ -66,7 +66,20 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "threshold2": 400, + "threshold2Color": "red", + "thresholdLine": true + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +123,20 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 100, + "threshold1Color": "green", + "threshold2": 300, + "threshold2Color": "blue", + "thresholdLine": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +180,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -198,7 +235,25 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + }, + "thresholds": [ + { + "color": "purple", + "colorMode": "custom", + "value": 50 + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -242,7 +297,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -351,5 +411,10 @@ "title": "V13 Graph Thresholds Migration Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2beta1.json index a41d04804b7..0fdbbd1990b 100644 --- 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 @@ -69,7 +69,20 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "threshold2": 400, + "threshold2Color": "red", + "thresholdLine": true + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +128,20 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 100, + "threshold1Color": "green", + "threshold2": 300, + "threshold2Color": "blue", + "thresholdLine": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +187,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -207,7 +244,25 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + }, + "thresholds": [ + { + "color": "purple", + "colorMode": "custom", + "value": 50 + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -253,7 +308,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -362,5 +422,10 @@ "title": "V13 Graph Thresholds Migration Test", "variables": [] }, - "status": {} + "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 index 3edc16edae6..b2673944cb7 100644 --- 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 @@ -72,7 +72,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -129,5 +134,10 @@ "title": "Dashboard with minimal graph panel settings", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2beta1.json index 8e18ac60c4d..1b8e98bafc0 100644 --- 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 @@ -75,7 +75,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -132,5 +137,10 @@ "title": "Dashboard with minimal graph panel settings", "variables": [] }, - "status": {} + "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 index e4a62f00bc7..5a00a12baa3 100644 --- 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 @@ -68,7 +68,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -114,7 +125,20 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "max": 100, + "min": 0, + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -210,5 +234,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json index 975fc24c969..9251f743242 100644 --- 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 @@ -71,7 +71,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -119,7 +130,20 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "max": 100, + "min": 0, + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -217,5 +241,10 @@ } ] }, - "status": {} + "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.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v0alpha1.json index 6c259253e55..35cc948844c 100644 --- 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 @@ -743,7 +743,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": true, "label": "cluster", @@ -764,7 +766,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": false, "label": "namespace", 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 index decdff12902..26945033c4c 100644 --- 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 @@ -961,8 +961,12 @@ "hide": "dontHide", "refresh": "onDashboardLoad", "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "$datasource" + }, "query": { - "kind": "prometheus", + "kind": "", "spec": { "__legacyStringValue": "label_values(up, job)" } @@ -988,8 +992,12 @@ "hide": "dontHide", "refresh": "onDashboardLoad", "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "$datasource" + }, "query": { - "kind": "prometheus", + "kind": "", "spec": { "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" } @@ -1004,5 +1012,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json index 9db5193d5af..344b0ef564a 100644 --- 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 @@ -978,8 +978,11 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", + "datasource": { + "name": "$datasource" + }, "spec": { "__legacyStringValue": "label_values(up, job)" } @@ -1007,8 +1010,11 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", + "datasource": { + "name": "$datasource" + }, "spec": { "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" } @@ -1023,5 +1029,10 @@ } ] }, - "status": {} + "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 index 347556eb45d..551348d7852 100644 --- 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 @@ -83,7 +83,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -127,7 +138,20 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -223,5 +247,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json index 756f4a76074..c6ab962de2f 100644 --- 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 @@ -87,7 +87,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -133,7 +144,20 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -231,5 +255,10 @@ } ] }, - "status": {} + "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 index 1bf45848829..4c21c63a66a 100644 --- 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 @@ -1455,5 +1455,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2beta1.json index d7f5fd82659..684b7b0f564 100644 --- 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 @@ -1481,5 +1481,10 @@ } ] }, - "status": {} + "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 index 6cf8402f377..c4198fa24b9 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +115,14 @@ "kind": "logs", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "logs", + "originalOptions": { + "height": 100 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +166,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -198,7 +215,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -254,7 +276,14 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "height": 200 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -282,7 +311,14 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "height": 200 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -719,5 +755,10 @@ "title": "V16 Grid Layout Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2beta1.json index 2a6622f27a4..578e9d707ad 100644 --- 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 @@ -69,7 +69,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +120,14 @@ "group": "logs", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "logs", + "originalOptions": { + "height": 100 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +173,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -207,7 +224,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -265,7 +287,14 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "height": 200 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -294,7 +323,14 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "height": 200 + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -739,5 +775,10 @@ "title": "V16 Grid Layout Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 221b01ffec5..52c19734101 100644 --- 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 @@ -1655,5 +1655,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2beta1.json index 7c05e2ab74c..49e7fb1d9e7 100644 --- 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 @@ -1707,5 +1707,10 @@ } ] }, - "status": {} + "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 index aa9017f0720..ecc7830b8fa 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +115,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +164,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -242,7 +257,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -374,7 +394,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -462,7 +487,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -636,5 +666,10 @@ "title": "V17 MinSpan to MaxPerRow Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2beta1.json index 697c0dadaac..ce68572f0d2 100644 --- 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 @@ -69,7 +69,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +120,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +171,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -253,7 +268,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -391,7 +411,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -483,7 +508,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -657,5 +687,10 @@ "title": "V17 MinSpan to MaxPerRow Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index f154e2e2679..55f7729fc1a 100644 --- 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 @@ -288,6 +288,10 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "show": true, "showLegend": true @@ -401,5 +405,10 @@ "title": "V18 Gauge Options Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2beta1.json index 6d1528a868a..758bdfad09e 100644 --- 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 @@ -299,6 +299,10 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "show": true, "showLegend": true @@ -412,5 +416,10 @@ "title": "V18 Gauge Options Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index e51fc93636a..596bd229b79 100644 --- 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 @@ -71,7 +71,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -312,7 +317,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -434,5 +444,10 @@ "title": "V19 Panel Links Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2beta1.json index 0dbf12ef898..1302e17c7f3 100644 --- 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 @@ -74,7 +74,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -325,7 +330,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -447,5 +457,10 @@ "title": "V19 Panel Links Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 129ba30c694..d30c3ce7923 100644 --- 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 @@ -68,7 +68,20 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graphite", + "originalOptions": { + "grid": { + "max": 100, + "min": 0 + }, + "legend": true, + "y2_format": "short", + "y_format": "percent" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -114,7 +127,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "min": 0 + }, + "legend": false, + "y_format": "bytes" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -158,7 +182,20 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table", + "originalOptions": { + "grid": { + "max": 100, + "min": 0 + }, + "legend": true, + "y2_format": "bytes", + "y_format": "short" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -204,7 +241,15 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graphite", + "originalOptions": { + "legend": true, + "y2_format": "Bps" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -354,5 +399,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json index 0c80ae34850..6a02d64f5b2 100644 --- 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 @@ -71,7 +71,20 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graphite", + "originalOptions": { + "grid": { + "max": 100, + "min": 0 + }, + "legend": true, + "y2_format": "short", + "y_format": "percent" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -119,7 +132,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "grid": { + "min": 0 + }, + "legend": false, + "y_format": "bytes" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -165,7 +189,20 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table", + "originalOptions": { + "grid": { + "max": 100, + "min": 0 + }, + "legend": true, + "y2_format": "bytes", + "y_format": "short" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -213,7 +250,15 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graphite", + "originalOptions": { + "legend": true, + "y2_format": "Bps" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -365,5 +410,10 @@ } ] }, - "status": {} + "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 index 95d3f1d8834..1e077553a81 100644 --- 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 @@ -426,5 +426,10 @@ "title": "V20 Variable Syntax Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2beta1.json index f5c9e157c75..13327bcd9f6 100644 --- 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 @@ -437,5 +437,10 @@ "title": "V20 Variable Syntax Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 0b2251f0dca..a93d077fef3 100644 --- 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 @@ -181,6 +181,10 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "dataLinks": [ { "title": "Graph Data Link", @@ -401,5 +405,10 @@ "title": "V21 Data Links Series to Field Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2beta1.json index 8c9808adf32..6244177502b 100644 --- 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 @@ -188,6 +188,10 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "dataLinks": [ { "title": "Graph Data Link", @@ -412,5 +416,10 @@ "title": "V21 Data Links Series to Field Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index dd03601b0ac..10c570651e5 100644 --- 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 @@ -66,7 +66,25 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "pattern": "Time", + "type": "number" + }, + { + "align": "auto", + "pattern": "Value", + "type": "string" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -123,5 +141,10 @@ "title": "V22 Table Panel Styles Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2beta1.json index fd3dc0b587b..8c7d86f1d86 100644 --- 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 @@ -69,7 +69,25 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "pattern": "Time", + "type": "number" + }, + { + "align": "auto", + "pattern": "Value", + "type": "string" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -126,5 +144,10 @@ "title": "V22 Table Panel Styles Test", "variables": [] }, - "status": {} + "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 index a40797444f4..3e93dbc7a66 100644 --- 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 @@ -374,5 +374,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json index a97d9a51e2f..7286db67bf2 100644 --- 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 @@ -391,5 +391,10 @@ } ] }, - "status": {} + "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 index c9dd5bffa51..6fa76798215 100644 --- 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 @@ -81,7 +81,29 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "legend": true, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20", + "30" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -125,7 +147,31 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "center", + "pattern": "/.*/" + }, + { + "align": "left", + "pattern": "LeftColumn" + }, + { + "align": "right", + "pattern": "RightColumn" + }, + { + "align": "auto", + "pattern": "AutoColumn" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -169,7 +215,35 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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$/" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -213,7 +287,37 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -257,7 +361,37 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -301,7 +435,29 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table", + "originalOptions": { + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20" + ], + "unit": "short" + } + ], + "table": "table2" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -389,7 +545,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -433,7 +594,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -477,7 +643,57 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -521,7 +737,47 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ], + "styles": [ + { + "decimals": 1, + "pattern": "/.*/", + "unit": "percent" + } + ], + "transform": "timeseries_aggregations" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -565,7 +821,20 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -609,7 +878,20 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "bytes" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -653,7 +935,20 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "pattern": "/.*/" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -712,7 +1007,20 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -756,7 +1064,29 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "colors": [ + "green", + "yellow", + "orange", + "red" + ], + "pattern": "/.*/", + "thresholds": [ + 10, + "20", + 30.5 + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -800,7 +1130,31 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "colorMode": "cell", + "pattern": "/.*/" + }, + { + "colorMode": "cell", + "pattern": "CellColumn" + }, + { + "colorMode": "row", + "pattern": "RowColumn" + }, + { + "colorMode": "value", + "pattern": "ValueColumn" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1065,5 +1419,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2beta1.json index 3d74b493b8e..9ae87fe0f32 100644 --- 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 @@ -85,7 +85,29 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "legend": true, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20", + "30" + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -131,7 +153,31 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "center", + "pattern": "/.*/" + }, + { + "align": "left", + "pattern": "LeftColumn" + }, + { + "align": "right", + "pattern": "RightColumn" + }, + { + "align": "auto", + "pattern": "AutoColumn" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -177,7 +223,35 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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$/" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -223,7 +297,37 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -269,7 +373,37 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -315,7 +449,29 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table", + "originalOptions": { + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20" + ], + "unit": "short" + } + ], + "table": "table2" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -407,7 +563,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -453,7 +614,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -499,7 +665,57 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -545,7 +761,47 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "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" + } + ], + "styles": [ + { + "decimals": 1, + "pattern": "/.*/", + "unit": "percent" + } + ], + "transform": "timeseries_aggregations" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -591,7 +847,20 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -637,7 +906,20 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "bytes" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -683,7 +965,20 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "align": "auto", + "pattern": "/.*/" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -744,7 +1039,20 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -790,7 +1098,29 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "colors": [ + "green", + "yellow", + "orange", + "red" + ], + "pattern": "/.*/", + "thresholds": [ + 10, + "20", + 30.5 + ] + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -836,7 +1166,31 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "styles": [ + { + "colorMode": "cell", + "pattern": "/.*/" + }, + { + "colorMode": "cell", + "pattern": "CellColumn" + }, + { + "colorMode": "row", + "pattern": "RowColumn" + }, + { + "colorMode": "value", + "pattern": "ValueColumn" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1101,5 +1455,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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 index 3375a6bffe4..618d1193a1c 100644 --- 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 @@ -123,7 +123,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -217,5 +228,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json index b873fdea3a3..7fd6cdec538 100644 --- 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 @@ -130,7 +130,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -226,5 +237,10 @@ } ] }, - "status": {} + "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 index a135614baba..c7298296064 100644 --- 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 @@ -110,7 +110,15 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "# Angular Text 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": [] @@ -240,5 +248,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2beta1.json index 378ae69f16a..db2cfcb916e 100644 --- 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 @@ -115,7 +115,15 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "# Angular Text 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": [] @@ -247,5 +255,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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 index 51cdcba85c1..bab8c53ac6a 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -94,7 +99,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -207,5 +217,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2beta1.json index 3fdcd01a27c..79dab4989d1 100644 --- 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 @@ -69,7 +69,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -98,7 +103,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -211,5 +221,10 @@ } ] }, - "status": {} + "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 index 54384f8dc06..75831381989 100644 --- 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 @@ -131,5 +131,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json index 6626b919137..e79b7d1f7d7 100644 --- 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 @@ -134,5 +134,10 @@ } ] }, - "status": {} + "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 index 82fc46be585..14e59bfc9bc 100644 --- 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 @@ -81,7 +81,24 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -125,7 +142,28 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -184,7 +222,47 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "thresholds": "10,20,30", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -393,5 +471,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json index 4eff500bef6..4f376187bba 100644 --- 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 @@ -85,7 +85,24 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -131,7 +148,28 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -193,7 +231,47 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "thresholds": "10,20,30", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -406,5 +484,10 @@ } ] }, - "status": {} + "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 index 0873ad2190c..195ceebb6cc 100644 --- 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 @@ -81,7 +81,24 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -140,7 +157,24 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -184,7 +218,28 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -243,7 +298,47 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "thresholds": "10,20,30", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -289,7 +384,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -622,5 +778,10 @@ "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2beta1.json index 2b7d27024c6..fb221254e90 100644 --- 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 @@ -85,7 +85,24 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -147,7 +164,24 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "thresholds": "" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -193,7 +227,28 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "thresholds": "10,20,30" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -255,7 +310,47 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 + }, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "thresholds": "10,20,30", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -303,7 +398,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -642,5 +798,10 @@ "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 2688367b33b..5a096f20011 100644 --- 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 @@ -435,5 +435,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json index 8e57483ceb4..270c6912485 100644 --- 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 @@ -458,5 +458,10 @@ } ] }, - "status": {} + "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 index db33eea5445..112dae69255 100644 --- 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 @@ -294,5 +294,10 @@ "title": "V3 No-Op Migration - but tests ensuring panel IDs are unique", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2beta1.json index 4a38725abc3..0a3a97926b8 100644 --- 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 @@ -303,5 +303,10 @@ "title": "V3 No-Op Migration - but tests ensuring panel IDs are unique", "variables": [] }, - "status": {} + "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 index 60334f9ffb1..40e4072138c 100644 --- 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 @@ -280,6 +280,10 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "tooltip": { "mode": "single" } @@ -530,7 +534,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -689,5 +693,10 @@ "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2beta1.json index 6848d2d1714..9f5630252bf 100644 --- 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 @@ -289,6 +289,10 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "tooltip": { "mode": "single" } @@ -546,7 +550,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -705,5 +709,10 @@ "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index ac34174bfcb..6fe0fcc67c8 100644 --- 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 @@ -669,5 +669,10 @@ "title": "V31 LabelsToFields Merge Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2beta1.json index d7f0684597c..5ca1fbff96c 100644 --- 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 @@ -684,5 +684,10 @@ "title": "V31 LabelsToFields Merge Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 3c58166f6c4..369db4327b7 100644 --- 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 @@ -144,7 +144,18 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -310,5 +321,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json index 1611cea2464..1fd5d1638ec 100644 --- 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 @@ -151,7 +151,18 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "yAxes": [ + { + "show": true + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -320,5 +331,10 @@ } ] }, - "status": {} + "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 index d41d2d7bda3..b21c071f750 100644 --- 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 @@ -345,7 +345,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -548,7 +553,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -720,5 +725,10 @@ "title": "V33 Panel Datasource Name to Ref Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2beta1.json index fbb8d83b728..c541c51dc42 100644 --- 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 @@ -361,7 +361,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -574,7 +579,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -746,5 +751,10 @@ "title": "V33 Panel Datasource Name to Ref Test", "variables": [] }, - "status": {} + "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 index ad69702a739..bc705379491 100644 --- 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 @@ -1663,7 +1663,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -1900,5 +1900,10 @@ "title": "CloudWatch Multiple Statistics Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2beta1.json index 79ee3db369e..329e10bcd42 100644 --- 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 @@ -1727,7 +1727,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -1964,5 +1964,10 @@ "title": "CloudWatch Multiple Statistics Test Dashboard", "variables": [] }, - "status": {} + "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 index c06dd85bb25..cbb59009f10 100644 --- 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 @@ -595,5 +595,10 @@ "title": "X-Axis Visibility Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2beta1.json index f9be2a6cee1..fde3ba9c006 100644 --- 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 @@ -612,5 +612,10 @@ "title": "X-Axis Visibility Test Dashboard", "variables": [] }, - "status": {} + "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 index 474b22d4ffb..97a0dd7d19c 100644 --- 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 @@ -1029,5 +1029,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2beta1.json index 6465fae7ed0..24c49f553ae 100644 --- 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 @@ -1065,5 +1065,10 @@ } ] }, - "status": {} + "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 index eb254828d3e..7dc1d5c7069 100644 --- 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 @@ -111,6 +111,10 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "displayMode": "list", "showLegend": false @@ -207,6 +211,10 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "displayMode": "list", "showLegend": false @@ -629,5 +637,10 @@ "title": "V37 Legend Normalization Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2beta1.json index 2803cc68947..ba78f73005d 100644 --- 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 @@ -115,6 +115,10 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "displayMode": "list", "showLegend": false @@ -214,6 +218,10 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + }, "legend": { "displayMode": "list", "showLegend": false @@ -642,5 +650,10 @@ "title": "V37 Legend Normalization Test Dashboard", "variables": [] }, - "status": {} + "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 index 92f6ec2ef8e..89dd54e3bf6 100644 --- 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 @@ -443,7 +443,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -659,5 +664,10 @@ "title": "V38 Table Migration Comprehensive Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2beta1.json index 493b6147404..546c73663d6 100644 --- 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 @@ -455,7 +455,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -671,5 +676,10 @@ "title": "V38 Table Migration Comprehensive Test Dashboard", "variables": [] }, - "status": {} + "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 index a1fb5ca81af..92dce415c0d 100644 --- 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 @@ -443,7 +443,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -659,5 +664,10 @@ "title": "V38 Table Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2beta1.json index 81cc875df07..7d89cc8bbdd 100644 --- 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 @@ -455,7 +455,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -671,5 +676,10 @@ "title": "V38 Table Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index 37eb51fbdff..8d382676f16 100644 --- 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 @@ -217,7 +217,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -396,7 +401,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -586,5 +596,10 @@ "title": "V39 TimeSeriesTable Transformation Migration Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2beta1.json index 3b56960e026..fe92c7f9537 100644 --- 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 @@ -222,7 +222,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -406,7 +411,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -596,5 +606,10 @@ "title": "V39 TimeSeriesTable Transformation Migration Test Dashboard", "variables": [] }, - "status": {} + "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 index e6701e601c7..45615b7ae44 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +115,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +164,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -237,5 +252,10 @@ "title": "V4 No-Op Migration Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2beta1.json index b6d1aa7d200..3a7e5420f11 100644 --- 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 @@ -69,7 +69,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +120,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +171,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -244,5 +259,10 @@ "title": "V4 No-Op Migration Test", "variables": [] }, - "status": {} + "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 index 10f6bbf20f4..40df255245e 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Empty String Refresh Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2beta1.json index a6cabe62d43..44136818b17 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Empty String Refresh Test Dashboard", "variables": [] }, - "status": {} + "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 index 13a901e94af..e149f9f6a45 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Boolean False Refresh Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2beta1.json index 7b5f900ef36..0499fbc7328 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Boolean False Refresh Test Dashboard", "variables": [] }, - "status": {} + "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 index 04b94fc3919..95b61ef4e22 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Refresh Not Set Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2beta1.json index 1f09949f995..93ce85b17e1 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Refresh Not Set Test Dashboard", "variables": [] }, - "status": {} + "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 index 103fe15fdf3..fd250ce03c6 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Numeric Refresh Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2beta1.json index 6904267e72b..f81e1b004e9 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Numeric Refresh Test Dashboard", "variables": [] }, - "status": {} + "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 index 9fe42b778dd..94fa361257d 100644 --- 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 @@ -64,5 +64,10 @@ "title": "String Refresh Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2beta1.json index b1bd81269c1..f8818a9dbf3 100644 --- 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 @@ -65,5 +65,10 @@ "title": "String Refresh Test Dashboard", "variables": [] }, - "status": {} + "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 index 3524796f0aa..bd9f897d313 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Boolean Refresh Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2beta1.json index 76eea7dbbdc..eecd119a617 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Boolean Refresh Test Dashboard", "variables": [] }, - "status": {} + "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 index 9036e54f22d..ed1ea8dc001 100644 --- 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 @@ -64,5 +64,10 @@ "title": "No Time Picker Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2beta1.json index 54e2cfeef43..6b3b49c11e5 100644 --- 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 @@ -65,5 +65,10 @@ "title": "No Time Picker Test Dashboard", "variables": [] }, - "status": {} + "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 index 78e27ac4411..2806b7c6d8d 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Time Picker No Time Options Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2beta1.json index 52de1909de4..5b9595d8f74 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Time Picker No Time Options Test Dashboard", "variables": [] }, - "status": {} + "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 index 8115f687203..a48695c53ec 100644 --- 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 @@ -64,5 +64,10 @@ "title": "Time Picker Time Options Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2beta1.json index 9dffb1fc27e..3f31b897bf0 100644 --- 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 @@ -65,5 +65,10 @@ "title": "Time Picker Time Options Test Dashboard", "variables": [] }, - "status": {} + "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 index ee97abbd7e2..6aa423b4431 100644 --- 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 @@ -123,5 +123,10 @@ "title": "v42 Migration Test - Harky Must", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2beta1.json index 3d267c3135b..79648901fc2 100644 --- 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 @@ -125,5 +125,10 @@ "title": "v42 Migration Test - Harky Must", "variables": [] }, - "status": {} + "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 index d231c6561ad..e019782a68b 100644 --- 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 @@ -94,7 +94,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [ @@ -328,7 +333,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -461,5 +466,10 @@ "title": "v42 Migration Test - HideFrom Tooltip", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2beta1.json index a1e187676a5..f97c257c251 100644 --- 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 @@ -97,7 +97,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [ @@ -335,7 +340,7 @@ "kind": "RowsLayoutRow", "spec": { "title": "", - "collapse": true, + "collapse": false, "hideHeader": true, "layout": { "kind": "GridLayout", @@ -468,5 +473,10 @@ "title": "v42 Migration Test - HideFrom Tooltip", "variables": [] }, - "status": {} + "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 index 3c5f977cf19..a1fc7ad4752 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +115,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +164,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -237,5 +252,10 @@ "title": "V5 No-Op Migration Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2beta1.json index 10544c3c0fe..860ab47b3ea 100644 --- 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 @@ -69,7 +69,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +120,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +171,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -244,5 +259,10 @@ "title": "V5 No-Op Migration Test", "variables": [] }, - "status": {} + "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 index 5c98e01c0f0..6ca93a6768a 100644 --- 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 @@ -108,7 +108,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +159,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -332,5 +342,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json index 19fbf9be88f..0fa353e9a30 100644 --- 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 @@ -113,7 +113,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +166,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -344,5 +354,10 @@ } ] }, - "status": {} + "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 index 425516baea9..10b57eb4b69 100644 --- 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 @@ -142,5 +142,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2beta1.json index 72cb9aadb9b..252a510f233 100644 --- 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 @@ -146,5 +146,10 @@ "title": "No Title", "variables": [] }, - "status": {} + "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 index 0bf433b534a..1bdd08aecf5 100644 --- 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 @@ -119,7 +119,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -177,7 +182,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -247,5 +257,10 @@ "title": "V8 InfluxDB Query Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2beta1.json index d8ee1076605..ed5b9515a9f 100644 --- 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 @@ -122,7 +122,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -182,7 +187,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -252,5 +262,10 @@ "title": "V8 InfluxDB Query Test", "variables": [] }, - "status": {} + "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 index 0e4ea43800d..b69579db16b 100644 --- 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 @@ -66,7 +66,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -110,7 +115,12 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +164,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -237,5 +252,10 @@ "title": "V9 No-Op Migration Test", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2beta1.json index 45487aaebb9..2f8f9c79a0b 100644 --- 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 @@ -69,7 +69,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -115,7 +120,12 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +171,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -244,5 +259,10 @@ "title": "V9 No-Op Migration Test", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2alpha1.json index 59ed9c457a1..c7f21881b4c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2alpha1.json @@ -558,5 +558,10 @@ "title": "Annotation filtering", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2beta1.json index 26e6d792f59..b7a613fc48b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.annotation-filtering.v42.v2beta1.json @@ -570,5 +570,10 @@ "title": "Annotation filtering", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json index 5449d222a2c..b6addcc81ed 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json @@ -1368,5 +1368,10 @@ "title": "Multi-lane annotations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json index e23c11c69ef..f806e27a98f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json @@ -1393,5 +1393,10 @@ "title": "Multi-lane annotations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json index 253b6471087..5fb32832089 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json @@ -7244,5 +7244,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json index 54edeecb540..3ab6bb18228 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json @@ -7345,5 +7345,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2alpha1.json index 5b384c10afc..f2cd5feadf1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2alpha1.json @@ -13081,5 +13081,10 @@ "title": "Datasource tests - Elasticsearch migration", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2beta1.json index 898dc2b2489..5ba04ac7b23 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.v2beta1.json @@ -13289,5 +13289,10 @@ "title": "Datasource tests - Elasticsearch migration", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2alpha1.json index 9eab3b48079..7cf034e5479 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2alpha1.json @@ -1450,5 +1450,10 @@ "title": "Datasource tests - Elasticsearch simple", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2beta1.json index 35e24952e8a..e358643fc42 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.v2beta1.json @@ -1472,5 +1472,10 @@ "title": "Datasource tests - Elasticsearch simple", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2alpha1.json index 462481754b0..d30cf519eb9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2alpha1.json @@ -164,6 +164,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -239,5 +298,10 @@ "title": "Datasource tests - InfluxDB Logs", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2beta1.json index 7cfd839bf00..def63fe73f1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.v2beta1.json @@ -170,6 +170,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -245,5 +304,10 @@ "title": "Datasource tests - InfluxDB Logs", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json index 3b776907f83..7bd7146240e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json @@ -113,7 +113,78 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 2, + "grid": {}, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + }, + "zerofill": true + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -317,5 +388,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json index a6ebf10b8f0..943bd4c0a7b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json @@ -117,7 +117,78 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 2, + "grid": {}, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + }, + "zerofill": true + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -327,5 +398,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json index d8a362c5029..f6e20264866 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json @@ -615,5 +615,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json index 029c2bcf218..01d2f669fe1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json @@ -627,5 +627,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2alpha1.json index e5078c03a19..c3feede3aca 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2alpha1.json @@ -1552,5 +1552,10 @@ "title": "Datasource tests - Loki query splitting", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2beta1.json index c2651fc75a2..6ac4f48fde4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v2beta1.json @@ -1577,5 +1577,10 @@ "title": "Datasource tests - Loki query splitting", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2alpha1.json index 3faa6bf6d7b..0f908ced2aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2alpha1.json @@ -129,6 +129,74 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -178,7 +246,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -227,6 +333,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -296,6 +461,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -546,5 +770,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2beta1.json index b9acf971b1d..a346f009588 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.v2beta1.json @@ -138,6 +138,74 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -189,7 +257,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -240,6 +346,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -312,6 +477,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -566,5 +790,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2alpha1.json index 915a7a7f790..747942f6dc3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2alpha1.json @@ -147,7 +147,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -195,7 +253,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -243,7 +358,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -291,7 +463,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -339,7 +573,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -387,7 +683,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -435,7 +788,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -483,7 +898,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -531,7 +1006,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -579,7 +1116,38 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -627,7 +1195,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -675,7 +1303,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -723,7 +1414,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -771,7 +1525,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -819,7 +1636,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -867,7 +1747,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -915,7 +1858,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -963,7 +1969,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1011,7 +2079,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1059,7 +2187,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1107,7 +2297,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1156,7 +2408,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1205,7 +2495,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1254,7 +2582,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1303,7 +2669,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1351,7 +2755,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1399,7 +2861,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1447,7 +2966,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1514,7 +3095,79 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1562,7 +3215,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1629,7 +3344,79 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1677,7 +3464,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1725,7 +3570,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2280,5 +4183,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2beta1.json index f28e28bdded..fa5c52ae871 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.v2beta1.json @@ -159,7 +159,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -209,7 +267,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -259,7 +374,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -309,7 +481,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -359,7 +593,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -409,7 +705,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -459,7 +812,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -509,7 +924,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -559,7 +1034,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -609,7 +1146,38 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -659,7 +1227,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -709,7 +1337,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -759,7 +1450,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -809,7 +1563,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -859,7 +1676,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -909,7 +1789,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -959,7 +1902,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1009,7 +2015,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1059,7 +2127,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1109,7 +2237,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1159,7 +2349,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1210,7 +2462,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1261,7 +2551,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1312,7 +2640,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1363,7 +2729,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1413,7 +2817,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1463,7 +2925,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1513,7 +3032,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1583,7 +3164,79 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1633,7 +3286,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1703,7 +3418,79 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1753,7 +3540,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1803,7 +3648,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2360,5 +4263,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2alpha1.json index bd3a41d6f40..59f1c712177 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2alpha1.json @@ -128,6 +128,74 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -179,6 +247,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -249,6 +376,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -299,7 +485,46 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -548,5 +773,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2beta1.json index fbb9f11c316..3f782078e85 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.v2beta1.json @@ -137,6 +137,74 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -190,6 +258,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -263,6 +390,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -315,7 +501,46 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -568,5 +793,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2alpha1.json index 007f6f3f04c..ccfd35d981e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2alpha1.json @@ -147,7 +147,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -195,7 +253,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -243,7 +358,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -291,7 +463,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -339,7 +573,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -387,7 +683,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -435,7 +788,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -483,7 +898,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -531,7 +1006,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -579,7 +1116,38 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -627,7 +1195,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -675,7 +1303,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -723,7 +1414,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -771,7 +1525,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -819,7 +1636,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -867,7 +1747,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -915,7 +1858,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -963,7 +1969,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1011,7 +2079,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1060,7 +2188,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1109,7 +2275,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1158,7 +2362,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1207,7 +2449,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1255,7 +2535,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1303,7 +2641,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1351,7 +2746,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1399,7 +2856,79 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1447,7 +2976,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1495,7 +3086,79 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1543,7 +3206,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1591,7 +3312,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2123,5 +3902,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2beta1.json index 35767416230..643298fb85c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.v2beta1.json @@ -159,7 +159,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -209,7 +267,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -259,7 +374,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -309,7 +481,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -359,7 +593,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -409,7 +705,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -459,7 +812,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -509,7 +924,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -559,7 +1034,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -609,7 +1146,38 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -659,7 +1227,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -709,7 +1337,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -759,7 +1450,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -809,7 +1563,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -859,7 +1676,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -909,7 +1789,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -959,7 +1902,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1009,7 +2015,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1059,7 +2127,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1110,7 +2238,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1161,7 +2327,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1212,7 +2416,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1263,7 +2505,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1313,7 +2593,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1363,7 +2701,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1413,7 +2808,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1463,7 +2920,79 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1513,7 +3042,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1563,7 +3154,79 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1613,7 +3276,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1663,7 +3384,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2197,5 +3976,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2alpha1.json index 71e90cc1bac..893d135c144 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2alpha1.json @@ -68,6 +68,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -119,6 +178,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -194,5 +312,10 @@ "title": "Datasource tests - OpenTSDB", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2beta1.json index 9566977e001..5086e9f987d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.v2beta1.json @@ -72,6 +72,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -125,6 +184,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -200,5 +318,10 @@ "title": "Datasource tests - OpenTSDB", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2alpha1.json index ccd7b7cac51..deb7d353db8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2alpha1.json @@ -84,6 +84,65 @@ "spec": { "pluginVersion": "8.1.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -148,6 +207,65 @@ "spec": { "pluginVersion": "8.1.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -219,5 +337,10 @@ "title": "Datasource tests - OpenTSDB v2.3", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2beta1.json index dd46ba3407a..342f5a0a94c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.v2beta1.json @@ -88,6 +88,65 @@ "version": "8.1.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -154,6 +213,65 @@ "version": "8.1.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -225,5 +343,10 @@ "title": "Datasource tests - OpenTSDB v2.3", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2alpha1.json index dd5972aef39..0e222d96742 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2alpha1.json @@ -170,6 +170,74 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -221,6 +289,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -291,6 +418,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -341,7 +527,46 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -590,5 +815,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2beta1.json index 707c89428e3..9ece61d256d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.v2beta1.json @@ -179,6 +179,74 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -232,6 +300,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -305,6 +432,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -357,7 +543,46 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -610,5 +835,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2alpha1.json index 1b70b5d07eb..a7bec316030 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2alpha1.json @@ -147,7 +147,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -195,7 +253,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -243,7 +358,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -291,7 +463,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -339,7 +573,69 @@ "kind": "bargauge", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -387,7 +683,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -435,7 +788,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -483,7 +898,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -531,7 +1006,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -579,7 +1116,38 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -627,7 +1195,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -675,7 +1303,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -723,7 +1414,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -771,7 +1525,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -819,7 +1636,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -867,7 +1747,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -915,7 +1858,70 @@ "kind": "histogram", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -963,7 +1969,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1011,7 +2079,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1060,7 +2188,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1109,7 +2275,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1158,7 +2362,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1207,7 +2449,45 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1255,7 +2535,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1303,7 +2641,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1351,7 +2746,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1399,7 +2856,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1447,7 +2964,69 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1495,7 +3074,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1543,7 +3182,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1591,7 +3288,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2121,5 +3876,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2beta1.json index 7878e91dac5..ce0dd08d44d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.v2beta1.json @@ -159,7 +159,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -209,7 +267,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -259,7 +374,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -309,7 +481,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -359,7 +593,69 @@ "group": "bargauge", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -409,7 +705,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -459,7 +812,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -509,7 +924,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -559,7 +1034,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -609,7 +1146,38 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false + }, + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -659,7 +1227,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -709,7 +1337,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -759,7 +1450,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 100, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -809,7 +1563,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -859,7 +1676,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -909,7 +1789,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 50, + "mode": "histogram", + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -959,7 +1902,70 @@ "group": "histogram", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "buckets": 20, + "mode": "histogram", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1009,7 +2015,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1059,7 +2127,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1110,7 +2238,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1161,7 +2327,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1212,7 +2416,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1263,7 +2505,45 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1313,7 +2593,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1363,7 +2701,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1413,7 +2808,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1463,7 +2920,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1513,7 +3030,69 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1563,7 +3142,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1613,7 +3252,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1663,7 +3360,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -2195,5 +3950,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2alpha1.json index 58f0b2cf4df..d7dded5b99f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2alpha1.json @@ -1371,5 +1371,10 @@ "title": "Bar Gauge Demo Unfilled", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2beta1.json index b1046fe5ca3..f1d552852e8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.v2beta1.json @@ -1434,5 +1434,10 @@ "title": "Bar Gauge Demo Unfilled", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2alpha1.json index 9f0da1593eb..d31a3f25ea2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2alpha1.json @@ -1698,5 +1698,10 @@ "title": "TestData - Demo Dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2beta1.json index 3e11006d1de..74943a89cf9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.v2beta1.json @@ -1745,5 +1745,10 @@ "title": "TestData - Demo Dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2alpha1.json index ca657342364..e557b4f80a6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2alpha1.json @@ -2880,5 +2880,10 @@ "title": "New Features in v7.4", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2beta1.json index 0c8b7143a91..0ad4e83973b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.v2beta1.json @@ -2955,5 +2955,10 @@ "title": "New Features in v7.4", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2alpha1.json index dcfd34332f4..ad6e731fd8d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2alpha1.json @@ -3951,5 +3951,10 @@ "title": "New Features in v8.0", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2beta1.json index 93b501b2222..4f087f02750 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.v2beta1.json @@ -4072,5 +4072,10 @@ "title": "New Features in v8.0", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2alpha1.json index 28ea4680f2e..160c0cdf458 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2alpha1.json @@ -673,5 +673,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2beta1.json index af64f4a2f02..8d1436def13 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.v2beta1.json @@ -687,5 +687,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2alpha1.json index 5f7f66dd3cc..1629c425da7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2alpha1.json @@ -261,5 +261,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2beta1.json index a4681683ba5..fd375b98fed 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.v2beta1.json @@ -263,5 +263,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2alpha1.json index f8f9922016b..dde9ab81f9c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2alpha1.json @@ -269,5 +269,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2beta1.json index 96c5247c4f4..04266fae41b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.v2beta1.json @@ -274,5 +274,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2alpha1.json index e7e365773f1..f1ef5ec1c11 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2alpha1.json @@ -326,5 +326,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2beta1.json index 8cb12e2e45f..8344ab2fd3e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.v2beta1.json @@ -331,5 +331,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2alpha1.json index 62c2ef1f9bc..b3c25fda752 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2alpha1.json @@ -326,5 +326,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2beta1.json index 4e7764ff205..facb775e9f1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.v2beta1.json @@ -331,5 +331,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2alpha1.json index cc3254f517b..f4a9ccc966a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2alpha1.json @@ -210,5 +210,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2beta1.json index df7016f1969..79c21a744ec 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.v2beta1.json @@ -212,5 +212,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2alpha1.json index 3d8e275556b..d3e309f9d05 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2alpha1.json @@ -469,5 +469,10 @@ "title": "Link Extensions (onClick)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2beta1.json index a783ef609f2..2d1d0f2e529 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.v2beta1.json @@ -482,5 +482,10 @@ "title": "Link Extensions (onClick)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2alpha1.json index 98e0b3fd047..fc48dd77960 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2alpha1.json @@ -327,5 +327,10 @@ "title": "Link Extensions (path)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2beta1.json index 5376f4e3832..8c998ee4b2c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.v2beta1.json @@ -333,5 +333,10 @@ "title": "Link Extensions (path)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2alpha1.json index 2f913de363f..1ed897f3cc8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2alpha1.json @@ -1167,5 +1167,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2beta1.json index 614ac4cabec..40cc11e8cb9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.v2beta1.json @@ -1201,5 +1201,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2alpha1.json index 6b40487f1d0..2b16e9ce76b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2alpha1.json @@ -168,5 +168,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2beta1.json index 0bd988b9902..59e7b0a96bc 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.v2beta1.json @@ -172,5 +172,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2alpha1.json index abab61bd552..7480afb75db 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2alpha1.json @@ -205,5 +205,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2beta1.json index 4fc9cedb62a..cd93214ebc3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.v2beta1.json @@ -209,5 +209,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2alpha1.json index 2d423c83c3d..f8f7fbc5350 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2alpha1.json @@ -565,5 +565,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2beta1.json index 2be4b39a06f..897abd7a0c7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.v2beta1.json @@ -574,5 +574,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2alpha1.json index 17ee9515ded..85cb38c13ff 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2alpha1.json @@ -485,5 +485,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2beta1.json index fbe90caa7bd..a1ec03cd26a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.v2beta1.json @@ -491,5 +491,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2alpha1.json index 05b4c6ed70c..123f410cac0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2alpha1.json @@ -136,5 +136,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2beta1.json index c9ba98c312b..0f3f2ab00a2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.v2beta1.json @@ -140,5 +140,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2alpha1.json index 16148c1ab23..4a9e24d85f6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2alpha1.json @@ -86,6 +86,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [ { "targetBlank": false, @@ -303,7 +361,15 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data link variables overview\n\nThis dashboard presents variables that one can use when creating *data links*. All links redirect to this dashboard and this panel represents the values that were interpolated in the link that was clicked.\n\n\n#### Series variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$seriesName\u003c/span\u003e\n2. **label.datacenter:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenter\u003c/span\u003e\n3. **label.datacenter.region:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenterRegion\u003c/span\u003e\n\n#### Field variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$fieldName\u003c/span\u003e\n\n#### Value variables\n1. **Time:** \u003cspan style=\"color: orange;\"\u003e$valueTime\u003c/span\u003e\n2. **Numeric:** \u003cspan style=\"color: orange;\"\u003e$valueNumeric\u003c/span\u003e\n3. **Text:** \u003cspan style=\"color: orange;\"\u003e$valueText\u003c/span\u003e\n4. **Calc:** \u003cspan style=\"color: orange;\"\u003e$valueCalc\u003c/span\u003e\n\n", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -354,6 +420,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [ { "targetBlank": false, @@ -579,5 +703,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2beta1.json index 1f8d4d5eee6..62b30aa6480 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.v2beta1.json @@ -91,6 +91,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [ { "targetBlank": false, @@ -314,7 +372,15 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data link variables overview\n\nThis dashboard presents variables that one can use when creating *data links*. All links redirect to this dashboard and this panel represents the values that were interpolated in the link that was clicked.\n\n\n#### Series variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$seriesName\u003c/span\u003e\n2. **label.datacenter:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenter\u003c/span\u003e\n3. **label.datacenter.region:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenterRegion\u003c/span\u003e\n\n#### Field variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$fieldName\u003c/span\u003e\n\n#### Value variables\n1. **Time:** \u003cspan style=\"color: orange;\"\u003e$valueTime\u003c/span\u003e\n2. **Numeric:** \u003cspan style=\"color: orange;\"\u003e$valueNumeric\u003c/span\u003e\n3. **Text:** \u003cspan style=\"color: orange;\"\u003e$valueText\u003c/span\u003e\n4. **Calc:** \u003cspan style=\"color: orange;\"\u003e$valueCalc\u003c/span\u003e\n\n", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -367,6 +433,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [ { "targetBlank": false, @@ -592,5 +716,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2alpha1.json index 171b505001e..f754cbd766a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2alpha1.json @@ -62,6 +62,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -109,7 +167,15 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -163,7 +229,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -334,5 +461,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2beta1.json index 4b2a7a98790..8a8cffdf465 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.v2beta1.json @@ -67,6 +67,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -116,7 +174,15 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -172,7 +238,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -349,5 +476,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json index 837c5cbfa5a..b1dbd3de041 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json @@ -120,7 +120,7 @@ "value": [ { "title": "filter", - "url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" + "url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" } ] } @@ -167,6 +167,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -214,7 +273,15 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -264,7 +331,59 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -639,5 +758,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json index 2b61b379a04..9089dd1d1fb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json @@ -124,7 +124,7 @@ "value": [ { "title": "filter", - "url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" + "url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" } ] } @@ -174,6 +174,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -223,7 +282,15 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod", + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -276,7 +343,59 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -661,5 +780,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2alpha1.json index efa298e1ec2..9eaf955bb68 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2alpha1.json @@ -247,5 +247,10 @@ "title": "Test variable output", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2beta1.json index 17fa40a073b..395867814e0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.v2beta1.json @@ -255,5 +255,10 @@ "title": "Test variable output", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2alpha1.json index 8ae6fb9f2cd..81d61c2509b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2alpha1.json @@ -171,6 +171,65 @@ "spec": { "pluginVersion": "7.2.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -259,5 +318,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2beta1.json index b07bdc72f60..01936dded6b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.v2beta1.json @@ -177,6 +177,65 @@ "version": "7.2.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -265,5 +324,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2alpha1.json index 08f14e2614f..7eb15579259 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2alpha1.json @@ -84,6 +84,67 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -198,5 +259,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2beta1.json index a1ff094b67b..c89000daee7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.v2beta1.json @@ -89,6 +89,67 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -207,5 +268,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2alpha1.json index f71a17e660d..83c4dd25a5c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2alpha1.json @@ -536,5 +536,10 @@ "title": "Live flakey stream (w/ liveNow)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2beta1.json index 892812351b8..89bea6fc4d4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.v2beta1.json @@ -545,5 +545,10 @@ "title": "Live flakey stream (w/ liveNow)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2alpha1.json index abf4e4cdcef..017171df8ef 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2alpha1.json @@ -536,5 +536,10 @@ "title": "Live flakey stream", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2beta1.json index 68c0083c4ab..32cec1120c6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.v2beta1.json @@ -545,5 +545,10 @@ "title": "Live flakey stream", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2alpha1.json index f3777d0eedd..ca4649e18a8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2alpha1.json @@ -707,5 +707,10 @@ "title": "Live publish test", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2beta1.json index 66e94e73744..e87b98713e2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.v2beta1.json @@ -720,5 +720,10 @@ "title": "Live publish test", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2alpha1.json index efe66b835b3..ebe0f2098d8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2alpha1.json @@ -1034,5 +1034,10 @@ "title": "Live streaming examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2beta1.json index 5ad3b53ea98..051a06f0f94 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.v2beta1.json @@ -1054,5 +1054,10 @@ "title": "Live streaming examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v1beta1.json index 5ae52076acc..7542cf17652 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v1beta1.json @@ -296,6 +296,7 @@ } }, { + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", "datasource": { "type": "grafana-testdata-datasource" }, @@ -306,15 +307,7 @@ "y": 0 }, "id": 6, - "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" - }, + "mode": "markdown", "pluginVersion": "11.0.0-pre", "targets": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2alpha1.json index f8826a3a699..21d45e91f10 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2alpha1.json @@ -123,6 +123,42 @@ "spec": { "pluginVersion": "9.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + }, "cellHeight": "sm", "footer": { "countRows": false, @@ -270,6 +306,10 @@ "spec": { "pluginVersion": "9.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-piechart-panel", + "originalOptions": {} + }, "legend": { "displayMode": "list", "placement": "bottom", @@ -402,6 +442,41 @@ "spec": { "pluginVersion": "10.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-worldmap-panel", + "originalOptions": { + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 0, + "esMetric": "Count", + "hideEmpty": false, + "hideZero": false, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "mouseWheelZoom": false, + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "thresholds": "0,10", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + } + }, "basemap": { "name": "Basemap", "type": "default" @@ -562,6 +637,69 @@ "spec": { "pluginVersion": "11.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -667,6 +805,67 @@ "spec": { "pluginVersion": "11.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -775,6 +974,70 @@ "spec": { "pluginVersion": "11.3.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -880,6 +1143,65 @@ "spec": { "pluginVersion": "11.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -934,13 +1256,13 @@ "spec": { "pluginVersion": "11.0.0-pre", "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + } + } }, "fieldConfig": { "defaults": {}, @@ -1031,7 +1353,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1469,5 +1852,10 @@ "title": "Devenv - Panel migrations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2beta1.json index 8eea5775903..ed7123f5e38 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.v2beta1.json @@ -130,6 +130,42 @@ "version": "9.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + }, "cellHeight": "sm", "footer": { "countRows": false, @@ -283,6 +319,10 @@ "version": "9.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-piechart-panel", + "originalOptions": {} + }, "legend": { "displayMode": "list", "placement": "bottom", @@ -421,6 +461,41 @@ "version": "10.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-worldmap-panel", + "originalOptions": { + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 0, + "esMetric": "Count", + "hideEmpty": false, + "hideZero": false, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "mouseWheelZoom": false, + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "thresholds": "0,10", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + } + }, "basemap": { "name": "Basemap", "type": "default" @@ -587,6 +662,69 @@ "version": "11.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -698,6 +836,67 @@ "version": "11.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -811,6 +1010,70 @@ "version": "11.3.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -922,6 +1185,65 @@ "version": "11.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true, "legend": { "calcs": [], @@ -979,13 +1301,13 @@ "version": "11.0.0-pre", "spec": { "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + } + } }, "fieldConfig": { "defaults": {}, @@ -1082,7 +1404,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1520,5 +1903,10 @@ "title": "Devenv - Panel migrations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2alpha1.json index 78f2a2b6030..058a8806a5e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2alpha1.json @@ -859,5 +859,10 @@ "title": "BarChart - Panel Tests - Value sizing", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2beta1.json index c60ab8343a6..6c3fa8ae1a9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.v2beta1.json @@ -880,5 +880,10 @@ "title": "BarChart - Panel Tests - Value sizing", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2alpha1.json index 8f969cbef36..0f77a66f203 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2alpha1.json @@ -428,5 +428,10 @@ "title": "BarChart - Label Rotation \u0026 Skipping", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2beta1.json index 9290dd4ff14..3ad88c72861 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.v2beta1.json @@ -437,5 +437,10 @@ "title": "BarChart - Label Rotation \u0026 Skipping", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2alpha1.json index 522857ab19a..8bc7997b3d4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2alpha1.json @@ -535,5 +535,10 @@ "title": "BarChart - Panel Tests - Series toggle / bar widths", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2beta1.json index 9459814d20b..7a04ed00d31 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.v2beta1.json @@ -548,5 +548,10 @@ "title": "BarChart - Panel Tests - Series toggle / bar widths", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2alpha1.json index f25d5355ccc..e5369a162f7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2alpha1.json @@ -1682,5 +1682,10 @@ "title": "BarChart - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2beta1.json index 0d8c2dbf2a1..208c4c112c0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.v2beta1.json @@ -1721,5 +1721,10 @@ "title": "BarChart - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2alpha1.json index db478ad7e3d..bd95597bf13 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2alpha1.json @@ -1127,5 +1127,10 @@ "title": "Panel Tests - Bar Chart Tooltips \u0026 Legends", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2beta1.json index 4ea638cf0eb..3109835cd91 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.v2beta1.json @@ -1155,5 +1155,10 @@ "title": "Panel Tests - Bar Chart Tooltips \u0026 Legends", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2alpha1.json index 0e540c7e66e..290ee546192 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2alpha1.json @@ -1412,5 +1412,10 @@ "title": "Bar Gauge Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2beta1.json index cc190aaa2e0..ebfa6d31425 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.v2beta1.json @@ -1548,5 +1548,10 @@ "title": "Bar Gauge Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2alpha1.json index c88ce3b28e1..bd408ee62ee 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2alpha1.json @@ -1922,5 +1922,10 @@ "title": "Panel Tests - Bar Gauge", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2beta1.json index 1aa4b0e1a85..cc604bc2045 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.v2beta1.json @@ -2052,5 +2052,10 @@ "title": "Panel Tests - Bar Gauge", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2alpha1.json index 4f782695385..2aa2d935923 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2alpha1.json @@ -1214,5 +1214,10 @@ "title": "Panel Tests - Bar Gauge 2", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2beta1.json index 777a64366ba..92be6f0c11c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.v2beta1.json @@ -1262,5 +1262,10 @@ "title": "Panel Tests - Bar Gauge 2", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2alpha1.json index 97283f755fb..6d92fbd46bb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2alpha1.json @@ -640,5 +640,10 @@ "title": "Candlestick", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2beta1.json index 83ab2dd5571..0ce4c776fb5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.v2beta1.json @@ -653,5 +653,10 @@ "title": "Candlestick", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2alpha1.json index f5cb9c3c909..2976d37dc0f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2alpha1.json @@ -4064,5 +4064,10 @@ "title": "Panel Tests - Canvas Connection Examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2beta1.json index 49910daa683..47fb71205e2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.v2beta1.json @@ -4074,5 +4074,10 @@ "title": "Panel Tests - Canvas Connection Examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2alpha1.json index e5648af2325..c9e5b0662df 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2alpha1.json @@ -3503,5 +3503,10 @@ "title": "Panel Tests - Canvas Datalinks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2beta1.json index 8f705b659b6..d63bb93d321 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.v2beta1.json @@ -3506,5 +3506,10 @@ "title": "Panel Tests - Canvas Datalinks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2alpha1.json index b32f594a99e..8d2bdc0d36c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2alpha1.json @@ -4096,5 +4096,10 @@ "title": "Panel Tests - Canvas Examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2beta1.json index aa7b95b6dca..0c650fd4f87 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.v2beta1.json @@ -4113,5 +4113,10 @@ "title": "Panel Tests - Canvas Examples", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2alpha1.json index 7d0174aecaa..42c2a374b1c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2alpha1.json @@ -66,6 +66,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": "", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -115,6 +175,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -164,6 +284,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -213,6 +393,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -262,6 +502,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "µs", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -311,6 +609,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -360,6 +718,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -409,6 +825,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -458,6 +934,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -507,6 +1043,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -556,6 +1150,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -605,6 +1259,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -654,6 +1366,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -871,5 +1642,10 @@ "title": "Panel Tests - Auto Decimals", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2beta1.json index c2083797bdf..c1013a5bdcd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.v2beta1.json @@ -70,6 +70,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": "", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -121,6 +181,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -172,6 +292,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -223,6 +403,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -274,6 +514,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "µs", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -325,6 +623,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -376,6 +734,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -427,6 +843,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -478,6 +954,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -529,6 +1065,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -580,6 +1174,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -631,6 +1285,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -682,6 +1394,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -899,5 +1670,10 @@ "title": "Panel Tests - Auto Decimals", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2alpha1.json index 12d4efa68b6..bcb9fba2fe9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2alpha1.json @@ -471,5 +471,10 @@ "title": "Gradient Color modes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2beta1.json index 3c1e8803dda..54a0b0d198d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.v2beta1.json @@ -481,5 +481,10 @@ "title": "Gradient Color modes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2alpha1.json index 379d5597285..92b60103171 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2alpha1.json @@ -132,7 +132,68 @@ "kind": "stat", "spec": { "pluginVersion": "6.2.0-pre", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p99", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -176,7 +237,68 @@ "kind": "stat", "spec": { "pluginVersion": "6.2.0-pre", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p95", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -220,7 +342,68 @@ "kind": "stat", "spec": { "pluginVersion": "6.2.0-pre", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -302,6 +485,71 @@ "spec": { "pluginVersion": "6.2.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -347,6 +595,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -392,6 +700,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "red" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 0, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -476,6 +844,71 @@ "spec": { "pluginVersion": "6.2.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -521,6 +954,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -566,7 +1057,68 @@ "kind": "stat", "spec": { "pluginVersion": "6.2.0-pre", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -610,6 +1162,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -655,6 +1267,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -700,6 +1372,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -745,6 +1477,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -790,6 +1582,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -835,6 +1687,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "purple" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -880,6 +1792,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "orange" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -964,6 +1936,71 @@ "spec": { "pluginVersion": "6.2.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1009,6 +2046,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1054,6 +2151,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1099,6 +2256,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "purple" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1144,6 +2361,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "orange" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -2073,5 +3350,10 @@ "title": "Lazy Loading", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2beta1.json index 2767a76315e..b1b07fa0f56 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.v2beta1.json @@ -140,7 +140,68 @@ "group": "stat", "version": "6.2.0-pre", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p99", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -187,7 +248,68 @@ "group": "stat", "version": "6.2.0-pre", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p95", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -234,7 +356,68 @@ "group": "stat", "version": "6.2.0-pre", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -325,6 +508,71 @@ "version": "6.2.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -373,6 +621,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -421,6 +729,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "red" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 0, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -514,6 +882,71 @@ "version": "6.2.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -562,6 +995,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -610,7 +1101,68 @@ "group": "stat", "version": "6.2.0-pre", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "prefixFontSize": "80%", + "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": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -657,6 +1209,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -705,6 +1317,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -753,6 +1425,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -801,6 +1533,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -849,6 +1641,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -897,6 +1749,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "purple" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -945,6 +1857,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "orange" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1038,6 +2010,71 @@ "version": "6.2.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue", + "B-series": "dark-purple", + "C-series": "purple", + "Q-series": "dark-blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 3, + "fillGradient": 0, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1086,6 +2123,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1134,6 +2231,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "blue" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1182,6 +2339,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "purple" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -1230,6 +2447,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "orange" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -2229,5 +3506,10 @@ "title": "Lazy Loading", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2alpha1.json index 03cea1fc2c8..ce9d8e17187 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2alpha1.json @@ -1843,5 +1843,10 @@ "title": "Panel \u0026 data links in stat, gauge and bargauge", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2beta1.json index 6fbdb415483..fe5fbfda4aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.v2beta1.json @@ -1881,5 +1881,10 @@ "title": "Panel \u0026 data links in stat, gauge and bargauge", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2alpha1.json index 5723bef32ca..0572d6b73c0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2alpha1.json @@ -67,7 +67,68 @@ "kind": "stat", "spec": { "pluginVersion": "6.2.0-pre", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": false + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -258,7 +319,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -354,7 +473,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -515,7 +695,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -676,7 +917,65 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -819,7 +1118,68 @@ "kind": "stat", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1107,5 +1467,10 @@ "title": "Panel Tests - With \u0026 Without title", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2beta1.json index 93173c86f2b..deca04a6634 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.v2beta1.json @@ -71,7 +71,68 @@ "group": "stat", "version": "6.2.0-pre", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "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": false + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -268,7 +329,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -368,7 +487,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -534,7 +714,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -700,7 +941,65 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -847,7 +1146,68 @@ "group": "stat", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "singlestat", + "originalOptions": { + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "thresholds": "", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1137,5 +1497,10 @@ "title": "Panel Tests - With \u0026 Without title", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2alpha1.json index 15f76709e5e..0c5ddf56280 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2alpha1.json @@ -614,5 +614,10 @@ "title": "Datasource tests - Shared Queries", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2beta1.json index be9740c4b92..15ca6962fc8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.v2beta1.json @@ -630,5 +630,10 @@ "title": "Datasource tests - Shared Queries", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2alpha1.json index d28a40a68f1..08343d13d55 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2alpha1.json @@ -139,5 +139,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2beta1.json index aedaf24d379..aaf9456f795 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.v2beta1.json @@ -141,5 +141,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2alpha1.json index 7b820200d04..1a01292a17e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2alpha1.json @@ -138,5 +138,10 @@ "title": "Datagrid example", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2beta1.json index ec12c171f3a..fc5049c6d79 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.v2beta1.json @@ -144,5 +144,10 @@ "title": "Datagrid example", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2alpha1.json index 2ca31f3255f..69033e3dfc5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2alpha1.json @@ -130,5 +130,10 @@ "title": "Panel Tests - Flame Graph", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2beta1.json index 10b0b3ed930..982f4088303 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.v2beta1.json @@ -134,5 +134,10 @@ "title": "Panel Tests - Flame Graph", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2alpha1.json index f9046bea886..706d0bafd47 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2alpha1.json @@ -499,5 +499,10 @@ "title": "Panel Tests - Gauge Multi Series", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2beta1.json index da3a0f5e6f1..d0cbc9aa44e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.v2beta1.json @@ -534,5 +534,10 @@ "title": "Panel Tests - Gauge Multi Series", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2alpha1.json index bd213346a6a..fe4271d08e7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2alpha1.json @@ -62,6 +62,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -151,6 +157,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -241,6 +253,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -320,6 +338,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -401,6 +425,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -480,6 +510,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -559,6 +595,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -639,6 +681,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -730,6 +778,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -927,6 +981,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1006,6 +1066,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1085,6 +1151,12 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1429,5 +1501,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2beta1.json index 4e6d53c09ab..261cd5228db 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.v2beta1.json @@ -67,6 +67,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -159,6 +165,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -252,6 +264,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -334,6 +352,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -418,6 +442,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -500,6 +530,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -582,6 +618,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -665,6 +707,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -759,6 +807,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -961,6 +1015,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1043,6 +1103,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1125,6 +1191,12 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "gauge", + "originalOptions": { + "nullPointMode": "null" + } + }, "baseColor": "#299c46", "reduceOptions": { "calcs": [ @@ -1469,5 +1541,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 3084bbd32ea..66ce1cd0f3a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -77,14 +77,13 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -156,14 +155,13 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -235,14 +233,13 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -277,85 +274,6 @@ "title": "Center and bar glow", "type": "radialbar" }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 12, - "y": 1 - }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, { "datasource": { "type": "grafana-testdata-datasource" @@ -393,14 +311,13 @@ "id": 8, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -472,14 +389,13 @@ "id": 22, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -551,14 +467,13 @@ "id": 23, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -643,14 +558,13 @@ "id": 18, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -722,14 +636,13 @@ "id": 19, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -801,14 +714,13 @@ "id": 20, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -880,14 +792,13 @@ "id": 21, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -976,14 +887,13 @@ "id": 25, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1055,14 +965,13 @@ "id": 26, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1134,14 +1043,13 @@ "id": 29, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1213,14 +1121,13 @@ "id": 30, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1292,14 +1199,13 @@ "id": 28, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1388,14 +1294,13 @@ "id": 32, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1471,14 +1376,13 @@ "id": 34, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1554,14 +1458,13 @@ "id": 33, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1645,16 +1548,16 @@ "id": 9, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1668,8 +1571,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1731,16 +1633,15 @@ "id": 11, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1754,8 +1655,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1831,16 +1731,15 @@ "id": 13, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1854,8 +1753,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1918,17 +1816,15 @@ "id": 14, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1942,8 +1838,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2005,16 +1900,15 @@ "id": 15, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2028,8 +1922,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2091,16 +1984,15 @@ "id": 16, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2114,8 +2006,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 5b5c3951ad8..95850646c59 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -73,14 +73,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -165,16 +164,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -188,8 +186,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -262,16 +259,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -285,8 +281,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -360,17 +355,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -384,8 +377,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -459,16 +451,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -482,8 +473,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -556,16 +546,15 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -579,8 +568,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -653,14 +641,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -745,14 +732,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -837,14 +823,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -929,14 +914,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1021,14 +1005,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1113,14 +1096,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1201,14 +1183,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1293,14 +1274,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1385,14 +1365,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1477,14 +1456,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1573,14 +1551,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1661,14 +1638,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1753,14 +1729,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1849,14 +1824,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1945,14 +1919,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2045,106 +2018,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "fieldConfig": { - "defaults": { - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "green" - }, - { - "value": 80, - "color": "red" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - }, - "panel-5": { - "kind": "Panel", - "spec": { - "id": 5, - "title": "Spotlight", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "grafana-testdata-datasource", - "spec": { - "alias": "1", - "max": 100, - "min": 1, - "noise": 22, - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - }, - "refId": "A", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": { - "maxDataPoints": 20 - } - } - }, - "vizConfig": { - "kind": "radialbar", - "spec": { - "pluginVersion": "13.0.0-pre", - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2229,14 +2109,13 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2321,16 +2200,16 @@ "spec": { "pluginVersion": "13.0.0-pre", "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2344,8 +2223,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2429,19 +2307,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { @@ -2820,5 +2685,10 @@ "title": "Panel tests - Gauge (new)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 24125f197c5..fda0d31e71b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -77,14 +77,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -172,16 +171,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -195,8 +193,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "fieldConfig": { "defaults": { @@ -272,16 +269,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -295,8 +291,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -373,17 +368,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -397,8 +390,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -475,16 +467,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -498,8 +489,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -575,16 +565,15 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -598,8 +587,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -675,14 +663,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -770,14 +757,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -865,14 +851,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -960,14 +945,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1055,14 +1039,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1150,14 +1133,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1241,14 +1223,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1336,14 +1317,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1431,14 +1411,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1526,14 +1505,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1625,14 +1603,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1716,14 +1693,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1811,14 +1787,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1910,14 +1885,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2009,14 +1983,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2112,109 +2085,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "fieldConfig": { - "defaults": { - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "green" - }, - { - "value": 80, - "color": "red" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - }, - "panel-5": { - "kind": "Panel", - "spec": { - "id": 5, - "title": "Spotlight", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "grafana-testdata-datasource", - "version": "v0", - "spec": { - "alias": "1", - "max": 100, - "min": 1, - "noise": 22, - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - }, - "refId": "A", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": { - "maxDataPoints": 20 - } - } - }, - "vizConfig": { - "kind": "VizConfig", - "group": "radialbar", - "version": "13.0.0-pre", - "spec": { - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2302,14 +2179,13 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2397,16 +2273,16 @@ "version": "13.0.0-pre", "spec": { "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2420,8 +2296,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "fieldConfig": { "defaults": { @@ -2505,19 +2380,6 @@ } } }, - { - "kind": "GridLayoutItem", - "spec": { - "x": 12, - "y": 0, - "width": 4, - "height": 6, - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, { "kind": "GridLayoutItem", "spec": { @@ -2896,5 +2758,10 @@ "title": "Panel tests - Gauge (new)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json index ddad9707104..2dddd657c5f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json @@ -961,10 +961,8 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json index b485fbf84ed..db19ac588c1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json @@ -864,10 +864,8 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1614,5 +1612,10 @@ "title": "Panel tests - Old gauge to new", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json index 304058565f1..8ddc6feb297 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json @@ -901,10 +901,8 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1666,5 +1664,10 @@ "title": "Panel tests - Old gauge to new", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2alpha1.json index fe6b8e09647..8f6ae2065e2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2alpha1.json @@ -671,5 +671,10 @@ "title": "Geomap - color field Copy", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2beta1.json index ccd502e8e11..e567e6dbae2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.v2beta1.json @@ -683,5 +683,10 @@ "title": "Geomap - color field Copy", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2alpha1.json index f55700a79cd..135c8d98558 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2alpha1.json @@ -201,5 +201,10 @@ "title": "Panel Tests - Geomap Photo Layer", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2beta1.json index 4a7594b73a9..ce7d1c02a17 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.v2beta1.json @@ -205,5 +205,10 @@ "title": "Panel Tests - Geomap Photo Layer", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2alpha1.json index 92f520e0857..32cc829050d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2alpha1.json @@ -341,5 +341,10 @@ "title": "Panel Tests - Geomap Fit to Data - Multiple Layer Types", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2beta1.json index 82b86117485..758836fdd75 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.v2beta1.json @@ -347,5 +347,10 @@ "title": "Panel Tests - Geomap Fit to Data - Multiple Layer Types", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2alpha1.json index 87e64b32650..f6a93ab505e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2alpha1.json @@ -173,5 +173,10 @@ "title": "Panel Tests - Geomap geohash transformer", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2beta1.json index 5184ebb6680..376a2e0b52a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.v2beta1.json @@ -177,5 +177,10 @@ "title": "Panel Tests - Geomap geohash transformer", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2alpha1.json index 6ae35916b91..aecc89871f9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2alpha1.json @@ -457,5 +457,10 @@ "title": "Panel Tests - Geomap 9.1", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2beta1.json index a385b59567b..0bbab3d3711 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.v2beta1.json @@ -467,5 +467,10 @@ "title": "Panel Tests - Geomap 9.1", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2alpha1.json index f7374506e86..d4c2a4a8145 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2alpha1.json @@ -538,5 +538,10 @@ "title": "Panel Tests - Geomap Multi Layers", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2beta1.json index b244899e6d4..4726ddb6e4b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.v2beta1.json @@ -545,5 +545,10 @@ "title": "Panel Tests - Geomap Multi Layers", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2alpha1.json index 517c35c8e98..209afba2ff2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2alpha1.json @@ -594,5 +594,10 @@ "title": "Panel Tests - Geomap", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2beta1.json index 2d8549a8d89..4fdd338e7bd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.v2beta1.json @@ -607,5 +607,10 @@ "title": "Panel Tests - Geomap", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2alpha1.json index 44ab8d372ff..ef393dae560 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2alpha1.json @@ -66,7 +66,68 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 0, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -114,7 +175,68 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "rgb(87, 186, 242)" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 4, + "fillGradient": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -162,7 +284,68 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "rgb(48, 139, 237)" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -210,7 +393,68 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "red" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decgbytes", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -310,5 +554,10 @@ "title": "Panel Tests - Graph - Gradient Area Fills", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2beta1.json index 6beb26d4e70..e6def19e038 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.v2beta1.json @@ -70,7 +70,68 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "fillGradient": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 0, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -120,7 +181,68 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "rgb(87, 186, 242)" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 4, + "fillGradient": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -170,7 +292,68 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "rgb(48, 139, 237)" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -220,7 +403,68 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "red" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decgbytes", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -320,5 +564,10 @@ "title": "Panel Tests - Graph - Gradient Area Fills", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2alpha1.json index 4628e8234f0..6aab259eb23 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2alpha1.json @@ -63,6 +63,65 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -700,6 +759,65 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "celsius", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -971,5 +1089,10 @@ "title": "Panel Tests - shared tooltips", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2beta1.json index 540a9010d55..2ba9ec92cb6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.v2beta1.json @@ -67,6 +67,65 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -717,6 +776,65 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "celsius", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -990,5 +1108,10 @@ "title": "Panel Tests - shared tooltips", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2alpha1.json index 2dd4dd48907..894c33abef1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2alpha1.json @@ -61,7 +61,78 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -104,7 +175,140 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -150,7 +354,78 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -196,7 +471,73 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "red", + "fill": false, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -238,7 +579,76 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(234, 112, 112, 0.12)", + "from": "22:00", + "line": false, + "lineColor": "rgba(237, 46, 24, 0.60)", + "op": "time", + "to": "00:30" + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -351,5 +761,10 @@ "title": "Panel Tests - Graph Time Regions", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2beta1.json index 7a1aa009f08..69aceb23017 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.v2beta1.json @@ -66,7 +66,78 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -112,7 +183,140 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -161,7 +365,78 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -210,7 +485,73 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "red", + "fill": false, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -255,7 +596,76 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(234, 112, 112, 0.12)", + "from": "22:00", + "line": false, + "lineColor": "rgba(237, 46, 24, 0.60)", + "op": "time", + "to": "00:30" + } + ], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -368,5 +778,10 @@ "title": "Panel Tests - Graph Time Regions", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2alpha1.json index e251ae6c6db..fa1ae3a63d8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2alpha1.json @@ -62,7 +62,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -106,7 +166,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -183,7 +303,72 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -227,7 +412,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -271,7 +466,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Stacking values on top of nulls, should treat the null values as zero. ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -315,7 +520,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Stacking when all values are null should leave a gap in the graph", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -404,7 +619,66 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -583,7 +857,66 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -672,7 +1005,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -881,7 +1274,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -925,7 +1378,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -969,7 +1482,66 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1030,7 +1602,72 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1074,7 +1711,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1118,7 +1765,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1164,7 +1871,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1223,7 +1990,74 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1267,7 +2101,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Just verify that the tooltip time has millisecond resolution ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1311,7 +2155,17 @@ "kind": "text", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Verify that axis labels look ok", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1355,7 +2209,67 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1431,7 +2345,72 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1752,5 +2731,10 @@ "title": "Panel Tests - Graph", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2beta1.json index a87d37bc099..5a580bbaa59 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.v2beta1.json @@ -67,7 +67,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -114,7 +174,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -198,7 +318,72 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -244,7 +429,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -290,7 +485,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Stacking values on top of nulls, should treat the null values as zero. ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -336,7 +541,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Stacking when all values are null should leave a gap in the graph", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -434,7 +649,66 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -634,7 +908,66 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -732,7 +1065,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -966,7 +1359,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1013,7 +1466,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1060,7 +1573,66 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1126,7 +1698,72 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1172,7 +1809,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1219,7 +1866,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1268,7 +1975,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1332,7 +2099,74 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Percent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1378,7 +2212,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Just verify that the tooltip time has millisecond resolution ", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1424,7 +2268,17 @@ "group": "text", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "text", + "originalOptions": { + "content": "Verify that axis labels look ok", + "editable": true, + "error": false, + "mode": "markdown" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1471,7 +2325,67 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1554,7 +2468,72 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1875,5 +2854,10 @@ "title": "Panel Tests - Graph", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2alpha1.json index 33b1bd246ee..f060ff213d2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2alpha1.json @@ -66,6 +66,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": "", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -115,6 +175,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -164,6 +284,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -213,6 +391,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -262,6 +500,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -311,6 +609,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -360,6 +716,66 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -409,6 +825,64 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -458,6 +932,65 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -623,5 +1156,10 @@ "title": "Panel Tests - Graph - Y axis ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2beta1.json index bf16659bc49..8202a6a7dd7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.v2beta1.json @@ -70,6 +70,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": "", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -121,6 +181,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -172,6 +292,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -223,6 +401,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -274,6 +512,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -325,6 +623,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -376,6 +732,66 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": "10000", + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -427,6 +843,64 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -478,6 +952,65 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -643,5 +1176,10 @@ "title": "Panel Tests - Graph - Y axis ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2alpha1.json index a5567148d48..fb399db27f9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2alpha1.json @@ -1113,5 +1113,10 @@ "title": "Heatmap calculate (log)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2beta1.json index 32816087c1d..1721f0b1c1f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.v2beta1.json @@ -1133,5 +1133,10 @@ "title": "Heatmap calculate (log)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2alpha1.json index 69764686c12..8225aec6834 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2alpha1.json @@ -74,7 +74,44 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateViridis", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 4, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 2, + "format": "areaM2", + "logBase": 1, + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -116,7 +153,46 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": { + "cardRound": 50 + }, + "color": { + "cardColor": "#1F60C4", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 1, + "format": "kwatt", + "logBase": 1, + "show": true, + "width": "100" + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -158,7 +234,44 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#1F60C4", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": true, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 1, + "format": "kwatt", + "logBase": 1, + "show": true, + "width": "100" + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -204,7 +317,46 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateViridis", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 4, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 2, + "format": "areaM2", + "logBase": 1, + "max": "50", + "min": "20", + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -246,7 +398,44 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateBuGn", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "xAxis": { + "show": true + }, + "xBucketNumber": 10, + "yAxis": { + "format": "short", + "logBase": 2, + "show": true, + "splitFactor": 2 + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -288,7 +477,44 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateBuGn", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "xAxis": { + "show": true + }, + "xBucketNumber": 10, + "yAxis": { + "format": "short", + "logBase": 10, + "show": true, + "splitFactor": 5 + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -414,5 +640,10 @@ "title": "Legacy heatmap", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2beta1.json index 7450f5af8ed..d7ec2abb5e8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.v2beta1.json @@ -78,7 +78,44 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateViridis", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 4, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 2, + "format": "areaM2", + "logBase": 1, + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -123,7 +160,46 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": { + "cardRound": 50 + }, + "color": { + "cardColor": "#1F60C4", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 1, + "format": "kwatt", + "logBase": 1, + "show": true, + "width": "100" + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -168,7 +244,44 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#1F60C4", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": true, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 1, + "format": "kwatt", + "logBase": 1, + "show": true, + "width": "100" + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -216,7 +329,46 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateViridis", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 4, + "xAxis": { + "show": true + }, + "yAxis": { + "decimals": 2, + "format": "areaM2", + "logBase": 1, + "max": "50", + "min": "20", + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -261,7 +413,44 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateBuGn", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "xAxis": { + "show": true + }, + "xBucketNumber": 10, + "yAxis": { + "format": "short", + "logBase": 2, + "show": true, + "splitFactor": 2 + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -306,7 +495,44 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateBuGn", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "legend": { + "show": true + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": true + }, + "xAxis": { + "show": true + }, + "xBucketNumber": 10, + "yAxis": { + "format": "short", + "logBase": 10, + "show": true, + "splitFactor": 5 + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -432,5 +658,10 @@ "title": "Legacy heatmap", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json index fdcb368edc4..2eb67e36f2f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json @@ -542,5 +542,10 @@ "title": "Heatmap X axis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json index b6d51918cbc..acba4cedbc2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json @@ -554,5 +554,10 @@ "title": "Heatmap X axis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json index cc15b1c4c91..57fbcad9d99 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json @@ -1243,5 +1243,10 @@ "title": "Panel Tests - Histogram", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json index 81e56c3d318..5b2ee8d8df2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json @@ -1276,5 +1276,10 @@ "title": "Panel Tests - Histogram", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2alpha1.json index 2efffd9390c..dc0aab46631 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2alpha1.json @@ -109,6 +109,65 @@ "spec": { "pluginVersion": "7.4.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -193,5 +252,10 @@ "title": "Panel - Panel Library", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2beta1.json index 20438d25434..50419f5a194 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.v2beta1.json @@ -113,6 +113,65 @@ "version": "7.4.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -197,5 +256,10 @@ "title": "Panel - Panel Library", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2alpha1.json index 742bed31cf6..84c03baac66 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2alpha1.json @@ -770,5 +770,10 @@ "title": "Panel Tests - Pie chart", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2beta1.json index b79118ec1bb..4e946c6aee9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.v2beta1.json @@ -786,5 +786,10 @@ "title": "Panel Tests - Pie chart", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2alpha1.json index 97d355f227a..636612934c4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2alpha1.json @@ -119,7 +119,1073 @@ "kind": "grafana-polystat-panel", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_2", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": 1, + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": 1, + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "requests/min (rpm)", + "value": "reqpm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -228,7 +1294,1129 @@ "kind": "grafana-polystat-panel", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_4", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Milli Bitcoin (mBTC)", + "value": "currencymBTC" + }, + { + "text": "Micro Bitcoin (μBTC)", + "value": "currencyμBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Turkish Lira (₺)", + "value": "currencyTRY" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "CFP franc (XPF)", + "value": "currencyXPF" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "counts/sec (cps)", + "value": "cps" + }, + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "events/sec (eps)", + "value": "eps" + }, + { + "text": "messages/sec (mps)", + "value": "mps" + }, + { + "text": "records/sec (rps)", + "value": "recps" + }, + { + "text": "rows/sec (rps)", + "value": "rowsps" + }, + { + "text": "counts/min (cpm)", + "value": "cpm" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "requests/min (rpm)", + "value": "reqpm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + }, + { + "text": "events/min (epm)", + "value": "epm" + }, + { + "text": "messages/min (mpm)", + "value": "mpm" + }, + { + "text": "records/min (rpm)", + "value": "recpm" + }, + { + "text": "rows/min (rpm)", + "value": "rowspm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-Ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-Ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-Ampere reactive (VAr)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-Ampere reactive (kVAr)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -337,7 +2525,1090 @@ "kind": "grafana-polystat-panel", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_5", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [ + { + "animateMode": "all", + "clickThrough": "", + "compositeName": "comp", + "enabled": true, + "hideMembers": true, + "members": [ + { + "seriesName": "A-series" + }, + { + "seriesName": "B-series" + } + ], + "sanitizeURLEnabled": true, + "sanitizedURL": "", + "showName": true, + "showValue": true, + "thresholdLevel": 0 + } + ], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -424,5 +3695,10 @@ "title": "Panel Tests - Polystat", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2beta1.json index 46644077d0c..5916250ffd5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.v2beta1.json @@ -130,7 +130,1073 @@ "group": "grafana-polystat-panel", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_2", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": 1, + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": 1, + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "requests/min (rpm)", + "value": "reqpm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -250,7 +1316,1129 @@ "group": "grafana-polystat-panel", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_4", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Milli Bitcoin (mBTC)", + "value": "currencymBTC" + }, + { + "text": "Micro Bitcoin (μBTC)", + "value": "currencyμBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Turkish Lira (₺)", + "value": "currencyTRY" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "CFP franc (XPF)", + "value": "currencyXPF" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "counts/sec (cps)", + "value": "cps" + }, + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "events/sec (eps)", + "value": "eps" + }, + { + "text": "messages/sec (mps)", + "value": "mps" + }, + { + "text": "records/sec (rps)", + "value": "recps" + }, + { + "text": "rows/sec (rps)", + "value": "rowsps" + }, + { + "text": "counts/min (cpm)", + "value": "cpm" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "requests/min (rpm)", + "value": "reqpm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + }, + { + "text": "events/min (epm)", + "value": "epm" + }, + { + "text": "messages/min (mpm)", + "value": "mpm" + }, + { + "text": "records/min (rpm)", + "value": "recpm" + }, + { + "text": "rows/min (rpm)", + "value": "rowspm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-Ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-Ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-Ampere reactive (VAr)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-Ampere reactive (kVAr)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -370,7 +2558,1090 @@ "group": "grafana-polystat-panel", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-polystat-panel", + "originalOptions": { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_5", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [ + { + "animateMode": "all", + "clickThrough": "", + "compositeName": "comp", + "enabled": true, + "hideMembers": true, + "members": [ + { + "seriesName": "A-series" + }, + { + "seriesName": "B-series" + } + ], + "sanitizeURLEnabled": true, + "sanitizedURL": "", + "showName": true, + "showValue": true, + "thresholdLevel": 0 + } + ], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + }, + { + "text": "Vietnamese Dong (VND)", + "value": "currencyVND" + }, + { + "text": "Malaysian Ringgit (RM)", + "value": "currencyMYR" + }, + { + "text": "Bulgarian Lev (BGN)", + "value": "currencyBGN" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date \u0026 time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "inch (in)", + "value": "lengthin" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "pound (lb)", + "value": "masslb" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Fahrenheit (°F)", + "value": "fahrenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -457,5 +3728,10 @@ "title": "Panel Tests - Polystat", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2alpha1.json index acb35da6bb8..2074ff4fa12 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2alpha1.json @@ -2768,5 +2768,10 @@ "title": "Panel Tests - Stat", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2beta1.json index fb9f810cfaa..98b11ebadb3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.v2beta1.json @@ -2883,5 +2883,10 @@ "title": "Panel Tests - Stat", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json index 44db8ddd292..d90c8dc52cd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json @@ -1498,5 +1498,10 @@ "title": "StatusHistory - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json index 513bc4d476c..7aaa0fff33a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json @@ -1537,5 +1537,10 @@ "title": "StatusHistory - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json index 2697100f4cd..540f0d9e54d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json @@ -1893,5 +1893,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json index ec3f1d1ed5c..6c9aa023163 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json @@ -1939,5 +1939,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json index d2ec399b434..bccce10d162 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json @@ -2192,5 +2192,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json index a2ea184c554..5e186ef1443 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json @@ -2227,5 +2227,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2alpha1.json index 2bbd6a111bc..4863b46eeb3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2alpha1.json @@ -178,5 +178,10 @@ "title": "Panel Tests - Table - Markdown", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2beta1.json index 4a1f3a61afd..1964dd78543 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.v2beta1.json @@ -181,5 +181,10 @@ "title": "Panel Tests - Table - Markdown", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2alpha1.json index e637af09f52..cb0c2bbc16d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2alpha1.json @@ -3000,5 +3000,10 @@ "title": "Panel Tests - Table - Pagination", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2beta1.json index bc15692c4cb..d79b6c9e68b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.v2beta1.json @@ -3018,5 +3018,10 @@ "title": "Panel Tests - Table - Pagination", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json index 78012512792..e5e260fd150 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json @@ -824,5 +824,10 @@ "title": "Panel Tests - Table - Sparklines", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json index 352bab0c09b..ac15a298939 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json @@ -846,5 +846,10 @@ "title": "Panel Tests - Table - Sparklines", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2alpha1.json index 9d3a3f84f61..c706000d557 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2alpha1.json @@ -80,7 +80,85 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -142,7 +220,86 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -204,7 +361,99 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [ + { + "text": "Avg", + "value": "avg" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Current", + "value": "current" + } + ], + "fontSize": "100%", + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_aggregations" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -251,7 +500,65 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "row", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "/Color/", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -313,7 +620,92 @@ "kind": "table", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -426,5 +818,10 @@ "title": "Panel Tests - Table", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2beta1.json index 2b97a31452a..3ddb128c20e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.v2beta1.json @@ -87,7 +87,85 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -154,7 +232,86 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_rows" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -221,7 +378,99 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [ + { + "text": "Avg", + "value": "avg" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Current", + "value": "current" + } + ], + "fontSize": "100%", + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_aggregations" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -271,7 +520,65 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "row", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "/Color/", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -338,7 +645,92 @@ "group": "table", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "timeseries_to_columns" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -451,5 +843,10 @@ "title": "Panel Tests - Table", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2alpha1.json index 25624e5c6d1..4e35b317cd9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2alpha1.json @@ -1252,5 +1252,10 @@ "title": "Panel Tests - React Table", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2beta1.json index b8d5f8acae3..8911415d0fc 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.v2beta1.json @@ -1280,5 +1280,10 @@ "title": "Panel Tests - React Table", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json index b8e6725ca5a..1b6348c35d5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json @@ -2507,5 +2507,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json index 4a7ea3bcf33..a77140c5beb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json @@ -2550,5 +2550,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2alpha1.json index e0299a0be0f..3ac4d6901f4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2alpha1.json @@ -431,5 +431,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2beta1.json index a93a6c13852..6e8243f3131 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.v2beta1.json @@ -450,5 +450,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2alpha1.json index 15877291ee7..cb30e79d196 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2alpha1.json @@ -356,5 +356,10 @@ "title": "Panel Tests - StateTimeline - multiple frames with endTime", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2beta1.json index 1d857be3ea5..34c3f244b54 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.v2beta1.json @@ -365,5 +365,10 @@ "title": "Panel Tests - StateTimeline - multiple frames with endTime", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2alpha1.json index 7a2c436af82..13303a094ec 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2alpha1.json @@ -263,5 +263,10 @@ "title": "Panel Tests - StateTimeline - multiple frames with nulls", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2beta1.json index 4b14dcf2810..7d8376e9106 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.v2beta1.json @@ -267,5 +267,10 @@ "title": "Panel Tests - StateTimeline - multiple frames with nulls", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2alpha1.json index 522ec817718..82500d25a30 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2alpha1.json @@ -670,5 +670,10 @@ "title": "Timeline Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2beta1.json index 8e01aece508..89c7db392ea 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.v2beta1.json @@ -699,5 +699,10 @@ "title": "Timeline Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2alpha1.json index 7c8170cbcf8..9b4b2edfb80 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2alpha1.json @@ -548,5 +548,10 @@ "title": "Timeline Modes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2beta1.json index f5f4d53dba6..35d2643cc81 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.v2beta1.json @@ -566,5 +566,10 @@ "title": "Timeline Modes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json index ff492690353..8dff3c34ccf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json @@ -1498,5 +1498,10 @@ "title": "StateTimeline - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json index 6dcd28290db..3baa3d21130 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json @@ -1537,5 +1537,10 @@ "title": "StateTimeline - Thresholds \u0026 Mappings", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2alpha1.json index f18b9741878..6f3390c4d11 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2alpha1.json @@ -790,5 +790,10 @@ "title": "Panel Tests - TimeSeries - bars high density (stroke + fill)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2beta1.json index 797c81e39f0..00de0e2092b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.v2beta1.json @@ -805,5 +805,10 @@ "title": "Panel Tests - TimeSeries - bars high density (stroke + fill)", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2alpha1.json index d51c3443680..2d0e6b76950 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2alpha1.json @@ -1374,5 +1374,10 @@ "title": "Panel Tests - Graph NG - By value color schemes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2beta1.json index cc2f62859f9..a3f729f9c5f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.v2beta1.json @@ -1403,5 +1403,10 @@ "title": "Panel Tests - Graph NG - By value color schemes", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2alpha1.json index 766832b0cb7..ac083165532 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2alpha1.json @@ -1067,5 +1067,10 @@ "title": "Panel Tests - Timeseries - Supported input formats", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2beta1.json index da1784c2b52..25ed3a1cf9b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.v2beta1.json @@ -1076,5 +1076,10 @@ "title": "Panel Tests - Timeseries - Supported input formats", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json index 7ad3c386000..5b43876c65f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json @@ -799,5 +799,10 @@ "title": "Panel Tests - Graph NG - Gradient Area Fills", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json index 4def562428b..ca96f9d5720 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json @@ -813,5 +813,10 @@ "title": "Panel Tests - Graph NG - Gradient Area Fills", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json index 0f5a4c7473e..74cba148009 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json @@ -1215,5 +1215,10 @@ "title": "Panel Tests - GraphNG - Hue Gradients", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json index e7e54fd938b..7e64bc79ef3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json @@ -1235,5 +1235,10 @@ "title": "Panel Tests - GraphNG - Hue Gradients", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2alpha1.json index 9bf92a90234..718a96c265c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2alpha1.json @@ -4362,5 +4362,10 @@ "title": "Panel Tests - Graph NG - Discrete panels", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2beta1.json index 3678e2c1bb8..187a377bdf1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.v2beta1.json @@ -4462,5 +4462,10 @@ "title": "Panel Tests - Graph NG - Discrete panels", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2alpha1.json index 7bfea9b01ad..f1bff310950 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2alpha1.json @@ -660,5 +660,10 @@ "title": "Panel Tests - Timeseries - Out of range", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2beta1.json index b0ee8fe1273..e868bafcffd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.v2beta1.json @@ -676,5 +676,10 @@ "title": "Panel Tests - Timeseries - Out of range", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2alpha1.json index 19003b3c112..dbe2ae22ebd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2alpha1.json @@ -821,5 +821,10 @@ "title": "Panel Tests - shared tooltips cursor positioning", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2beta1.json index 17006d8b0ac..5763494659d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.v2beta1.json @@ -839,5 +839,10 @@ "title": "Panel Tests - shared tooltips cursor positioning", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2alpha1.json index 91ebfa89a29..3c8700d1c0a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2alpha1.json @@ -2849,5 +2849,10 @@ "title": "Panel Tests - Graph NG - softMin/softMax", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2beta1.json index b616f35f02f..5cd6604807a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.v2beta1.json @@ -2887,5 +2887,10 @@ "title": "Panel Tests - Graph NG - softMin/softMax", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2alpha1.json index d5de67f8b7e..59b356d1a36 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2alpha1.json @@ -1521,5 +1521,10 @@ "title": "Panel Tests - TimeSeries - stacking", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2beta1.json index b14bc2dfae4..07c5f127c32 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.v2beta1.json @@ -1566,5 +1566,10 @@ "title": "Panel Tests - TimeSeries - stacking", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2alpha1.json index c37056e01ec..4c92eef28c1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2alpha1.json @@ -4629,5 +4629,10 @@ "title": "TimeSeries \u0026 BarChart Stacking", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2beta1.json index 200a8362e4e..486f41cdce0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.v2beta1.json @@ -4818,5 +4818,10 @@ "title": "TimeSeries \u0026 BarChart Stacking", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2alpha1.json index 99bf0a4bba4..b15dedc73a8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2alpha1.json @@ -176,6 +176,88 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": false, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": false, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -339,6 +421,88 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 60, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -826,6 +990,88 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -876,6 +1122,88 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": false, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": false, + "op": "gt", + "value": 60, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -928,6 +1256,97 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 40, + "yaxis": "left" + }, + { + "$$hashKey": "object:40", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -980,6 +1399,92 @@ "spec": { "pluginVersion": "7.5.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(50, 161, 230, 0.13)", + "line": true, + "lineColor": "#B877D9", + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(184, 119, 217, 0.23)", + "line": true, + "lineColor": "rgba(93, 196, 31, 0.6)", + "op": "lt", + "value": 40, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -1185,5 +1690,10 @@ "title": "Panel Tests - GraphNG Thresholds", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2beta1.json index d1f32f0900d..0e9b32d82cf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.v2beta1.json @@ -182,6 +182,88 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": false, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": false, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -349,6 +431,88 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 60, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -846,6 +1010,88 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -898,6 +1144,88 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": false, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": false, + "op": "gt", + "value": 60, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -952,6 +1280,97 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 40, + "yaxis": "left" + }, + { + "$$hashKey": "object:40", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -1006,6 +1425,92 @@ "version": "7.5.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(50, 161, 230, 0.13)", + "line": true, + "lineColor": "#B877D9", + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(184, 119, 217, 0.23)", + "line": true, + "lineColor": "rgba(93, 196, 31, 0.6)", + "op": "lt", + "value": 40, + "yaxis": "left" + } + ], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "alertThreshold": true }, "fieldConfig": { @@ -1211,5 +1716,10 @@ "title": "Panel Tests - GraphNG Thresholds", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2alpha1.json index cf0c37a3a79..43271c576dd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2alpha1.json @@ -735,5 +735,10 @@ "title": "Panel Tests - GraphNG - Time Axis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2beta1.json index f106b3c8e1a..3674472eb53 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.v2beta1.json @@ -749,5 +749,10 @@ "title": "Panel Tests - GraphNG - Time Axis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2alpha1.json index 4a6a5b03287..27afd1ec327 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2alpha1.json @@ -1095,5 +1095,10 @@ "title": "Zero Decimals Y Ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2beta1.json index 0d41fc01b8f..ef2241fb8f3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.v2beta1.json @@ -1121,5 +1121,10 @@ "title": "Zero Decimals Y Ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2alpha1.json index d23c438178c..40bf202a1ae 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2alpha1.json @@ -1722,5 +1722,10 @@ "title": "Panel Tests - Graph NG - Y axis ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2beta1.json index 923573205a8..6e3377b4cc4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.v2beta1.json @@ -1765,5 +1765,10 @@ "title": "Panel Tests - Graph NG - Y axis ticks", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json index cb50db62a68..e50b453076a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json @@ -4371,5 +4371,10 @@ "title": "Panel Tests - Graph NG", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json index 249af246ac9..65105663c85 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json @@ -4445,5 +4445,10 @@ "title": "Panel Tests - Graph NG", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2alpha1.json index dda9209aa94..1439756b694 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2alpha1.json @@ -270,5 +270,10 @@ "title": "Panel Tests - Trend", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2beta1.json index 11683b31b98..65de2a79cf4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.v2beta1.json @@ -274,5 +274,10 @@ "title": "Panel Tests - Trend", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2alpha1.json index 53122268d1d..76278b42b57 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2alpha1.json @@ -1785,5 +1785,10 @@ "title": "Panel Tests - XY Chart Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2beta1.json index a4c1b86c07e..70a7b3b7537 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.v2beta1.json @@ -1825,5 +1825,10 @@ "title": "Panel Tests - XY Chart Demo", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2alpha1.json index 33d0b2ee9b2..ccb0c353c72 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2alpha1.json @@ -2478,5 +2478,10 @@ "title": "Panel Tests - XY Chart migrations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2beta1.json index a43b5c5e1e9..add31528134 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.v2beta1.json @@ -2515,5 +2515,10 @@ "title": "Panel Tests - XY Chart migrations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json index 7583af8160e..1d60f0ef9bf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json @@ -885,5 +885,10 @@ "title": "XYChart tooltip color test", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json index e10e9fc65ff..5a46646474d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json @@ -907,5 +907,10 @@ "title": "XYChart tooltip color test", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2alpha1.json index 38e58c03f3a..d203fb8e357 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2alpha1.json @@ -150,5 +150,10 @@ "title": "Mostly blank dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2beta1.json index 45c5b980f7f..8f1a85fee5f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.v2beta1.json @@ -153,5 +153,10 @@ "title": "Mostly blank dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2alpha1.json index a8c0c9f5617..7e915f70e20 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2alpha1.json @@ -250,5 +250,10 @@ "title": "Panel Tests - Relative time zone support", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2beta1.json index e8b38167a2a..545a6a234b4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.v2beta1.json @@ -257,5 +257,10 @@ "title": "Panel Tests - Relative time zone support", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2alpha1.json index a738ad9daba..addd25f950f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2alpha1.json @@ -86,7 +86,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -132,7 +189,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -178,7 +292,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -224,7 +395,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -270,7 +498,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -316,7 +601,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -362,7 +704,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -408,7 +807,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -454,7 +910,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -500,7 +1013,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -546,7 +1116,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -592,7 +1219,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -638,7 +1322,64 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -851,5 +1592,10 @@ "title": "Panel tests - Slow Queries \u0026 Annotations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2beta1.json index 2d018bf0db4..427a41ec1d7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.v2beta1.json @@ -93,7 +93,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -142,7 +199,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -191,7 +305,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -240,7 +411,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -289,7 +517,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -338,7 +623,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -387,7 +729,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -436,7 +835,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -485,7 +941,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -534,7 +1047,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -583,7 +1153,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -632,7 +1259,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -681,7 +1365,64 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -894,5 +1635,10 @@ "title": "Panel tests - Slow Queries \u0026 Annotations", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2alpha1.json index 027ae6f3352..5fd3f355cbb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2alpha1.json @@ -3416,5 +3416,10 @@ "title": "A tall dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2beta1.json index a225f2e7d81..b0d10fd280b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.v2beta1.json @@ -3517,5 +3517,10 @@ "title": "A tall dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2alpha1.json index 0d6468c31f1..f95a6880b61 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2alpha1.json @@ -108,6 +108,67 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -171,6 +232,73 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -217,6 +345,68 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -265,6 +455,68 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -326,6 +578,75 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Perecent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -404,6 +725,73 @@ "spec": { "pluginVersion": "", "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -532,5 +920,10 @@ "title": "Panel Tests - Time zone support", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2beta1.json index 348f2876085..46da655452b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.v2beta1.json @@ -119,6 +119,67 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "decimals": 3, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -187,6 +248,73 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -236,6 +364,68 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -287,6 +477,68 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -353,6 +605,75 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Perecent", + "logBase": 1, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -438,6 +759,73 @@ "version": "", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, "dataLinks": [] }, "fieldConfig": { @@ -566,5 +954,10 @@ "title": "Panel Tests - Time zone support", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2alpha1.json index 8808e6d73dc..a0e87e7d5f2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2alpha1.json @@ -1014,5 +1014,10 @@ "title": "Transforms - Config from query", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2beta1.json index 69930ae2e77..8134c573a69 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.v2beta1.json @@ -1040,5 +1040,10 @@ "title": "Transforms - Config from query", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2alpha1.json index 469fca5d2aa..a611e5b0db2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2alpha1.json @@ -418,5 +418,10 @@ "title": "Transforms - Test extractFields JSON", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2beta1.json index 396cbc7348b..e1229efccac 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.v2beta1.json @@ -428,5 +428,10 @@ "title": "Transforms - Test extractFields JSON", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json index 9202330a572..056fdc62383 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json @@ -227,5 +227,10 @@ "title": "Transforms - Filters", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json index c9d1b8f11c4..57c5559add1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json @@ -233,5 +233,10 @@ "title": "Transforms - Filters", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2alpha1.json index 9797be322d6..cbd2e994f15 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2alpha1.json @@ -865,5 +865,10 @@ "title": "Transforms - Join by field", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2beta1.json index ab0fb924f23..8fc053627fd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.v2beta1.json @@ -884,5 +884,10 @@ "title": "Transforms - Join by field", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2alpha1.json index 99353757d46..a10f2f6ed00 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2alpha1.json @@ -510,5 +510,10 @@ "title": "Transforms - Join by labels", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2beta1.json index fdc1b53ff36..73b06c4be7e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.v2beta1.json @@ -526,5 +526,10 @@ "title": "Transforms - Join by labels", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2alpha1.json index 22f053d066c..cab0fbea1e7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2alpha1.json @@ -1348,5 +1348,10 @@ "title": "Transforms - Regression analysis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2beta1.json index 0d00e3f1021..464769f0e9e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.v2beta1.json @@ -1371,5 +1371,10 @@ "title": "Transforms - Regression analysis", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2alpha1.json index fdea0031183..08b35165f79 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2alpha1.json @@ -741,5 +741,10 @@ "title": "Transforms - Reuse dashboard queries", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2beta1.json index 79002c2d12c..d8db8e1ada9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.v2beta1.json @@ -756,5 +756,10 @@ "title": "Transforms - Reuse dashboard queries", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2alpha1.json index 4d47c35593c..f89d509cae8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2alpha1.json @@ -963,5 +963,10 @@ "title": "Transforms - Rows to fields", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2beta1.json index ddeb72b78f0..234f5dcbd63 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.v2beta1.json @@ -983,5 +983,10 @@ "title": "Transforms - Rows to fields", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2alpha1.json index b9aea52ad62..43ca604b064 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2alpha1.json @@ -665,7 +665,42 @@ "kind": "heatmap", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": false + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "format": "short", + "logBase": 1, + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1578,5 +1613,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2beta1.json index 0bd3a37b2d8..ae693615b73 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.v2beta1.json @@ -691,7 +691,42 @@ "group": "heatmap", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "heatmap", + "originalOptions": { + "cards": {}, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateOranges", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "timeseries", + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "legend": { + "show": false + }, + "reverseYBuckets": false, + "tooltip": { + "show": true, + "showHistogram": false + }, + "xAxis": { + "show": true + }, + "yAxis": { + "format": "short", + "logBase": 1, + "show": true + }, + "yBucketBound": "auto" + } + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -1624,5 +1659,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2alpha1.json index d50e38918d2..7c77141f891 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2alpha1.json @@ -56,6 +56,14 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "panel-tests" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -94,6 +102,15 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "demo" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -133,6 +150,15 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "templating", + "gdev" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -172,6 +198,15 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "datasource-test" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -211,6 +246,12 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [] + } + }, "maxItems": 100, "query": "", "showHeadings": true, @@ -247,6 +288,15 @@ "spec": { "pluginVersion": "9.0.0-pre", "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "demo" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -379,5 +429,10 @@ "title": "Grafana Dev Overview \u0026 Home", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2beta1.json index dc06779b135..5cec6a9741b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.v2beta1.json @@ -58,6 +58,14 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "panel-tests" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -97,6 +105,15 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "demo" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -137,6 +154,15 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "templating", + "gdev" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -177,6 +203,15 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "datasource-test" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -217,6 +252,12 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [] + } + }, "maxItems": 100, "query": "", "showHeadings": true, @@ -254,6 +295,15 @@ "version": "9.0.0-pre", "spec": { "options": { + "__angularMigration": { + "autoMigrateFrom": "dashlist", + "originalOptions": { + "tags": [ + "gdev", + "demo" + ] + } + }, "maxItems": 1000, "query": "", "showHeadings": false, @@ -386,5 +436,10 @@ "title": "Grafana Dev Overview \u0026 Home", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v0alpha1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v0alpha1.json new file mode 100644 index 00000000000..d1253a0a81d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v0alpha1.json @@ -0,0 +1,1310 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "angular-migrations-test" + }, + "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": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate graph panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate graph panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate table (old) panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate table (old) panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate piechart panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate piechart panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate worldmap panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate worldmap panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate stat panel (TRUE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate stat panel (FALSE)", + "tooltip": "", + "type": "link", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate (TRUE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Auto migrate (FALSE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=false" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Disable angular (TRUE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.disableAngular=true" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Disable angular (FALSE)", + "tooltip": "", + "type": "link", + "url": "/d/cdd412c4/?__feature.disableAngular=false" + } + ], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 6, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 28, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.260951647569786,\n 1.887442338051216,\n 2.1526144400893514,\n 1.7287721375237766,\n 1.7262902137793208\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod1\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 1.589539045095202,\n 1.5914283506847613,\n 1.8976990616650726,\n 1.758223085999124,\n 2.2294649594813816\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod2\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.0914263380328766,\n 1.8164545521094575,\n 1.621111084665713,\n 1.3902653996444705,\n 1.482803315949775\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "barchart", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 11 + }, + "id": 29, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar chart panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "default": false, + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 32, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.3.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 28, + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode (with legend calcs)", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "bargauge", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "autoMigrateFrom": "graph", + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis histogram mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "histogram", + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 33, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar gauge panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 31, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Histogram panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "table-old", + "columns": [], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 33 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk_table" + } + ], + "title": "Table (old)", + "transform": "table", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 33 + }, + "id": 7, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Table (old) \u003e\u003e Table\n\nKnown issues:\n* wrapping text\n* style changes", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-singlestat-panel", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 43 + }, + "id": 9, + "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": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "thresholds": "", + "title": "grafana-singlestat-panel", + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "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": 23, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 43 + }, + "id": 10, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Singlestat \u003e\u003e Stat\n\nKnown issues:\n* limited options", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-piechart-panel", + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 51 + }, + "id": 24, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk_table" + } + ], + "title": "grafana-piechart-panel", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 51 + }, + "id": 25, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-piechart-panel \u003e\u003e piechart\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "autoMigrateFrom": "grafana-worldmap-panel", + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "decimals": 0, + "esMetric": "Count", + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 61 + }, + "hideEmpty": false, + "hideZero": false, + "id": 26, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "maxDataPoints": 1, + "mouseWheelZoom": false, + "options": { + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "pluginVersion": "10.4.0-pre", + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "targets": [ + { + "csvFileName": "flight_info_by_state.csv", + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_file" + } + ], + "thresholds": "0,10", + "title": "grafana-worldmap-panel", + "transformations": [ + { + "id": "merge", + "options": { + "reducers": [] + } + }, + { + "id": "reduce", + "options": { + "reducers": [ + "sum" + ] + } + } + ], + "type": "geomap", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 16, + "y": 61 + }, + "id": 27, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-worldmap-panel \u003e\u003e geomap\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [ + "gdev", + "migrations", + "angular" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Devenv - Panel migrations", + "uid": "cdd412c4", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2alpha1.json new file mode 100644 index 00000000000..d9e72ec700a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2alpha1.json @@ -0,0 +1,1861 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "angular-migrations-test" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "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": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "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-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table (old)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk_table" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "9.5.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + }, + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-23": { + "kind": "Panel", + "spec": { + "id": 23, + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "11.0.0-pre", + "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-24": { + "kind": "Panel", + "spec": { + "id": 24, + "title": "grafana-piechart-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk_table" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "piechart", + "spec": { + "pluginVersion": "9.5.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-piechart-panel", + "originalOptions": {} + }, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-25": { + "kind": "Panel", + "spec": { + "id": 25, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-piechart-panel \u003e\u003e piechart\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-26": { + "kind": "Panel", + "spec": { + "id": 26, + "title": "grafana-worldmap-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "csvFileName": "flight_info_by_state.csv", + "scenarioId": "csv_file" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "sum" + ] + } + } + } + ], + "queryOptions": { + "maxDataPoints": 1 + } + } + }, + "vizConfig": { + "kind": "geomap", + "spec": { + "pluginVersion": "10.4.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-worldmap-panel", + "originalOptions": { + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 0, + "esMetric": "Count", + "hideEmpty": false, + "hideZero": false, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "mouseWheelZoom": false, + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "thresholds": "0,10", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + } + }, + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-27": { + "kind": "Panel", + "spec": { + "id": 27, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-worldmap-panel \u003e\u003e geomap\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-28": { + "kind": "Panel", + "spec": { + "id": 28, + "title": "Flot graph - x axis series mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.260951647569786,\n 1.887442338051216,\n 2.1526144400893514,\n 1.7287721375237766,\n 1.7262902137793208\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod1\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 1.589539045095202,\n 1.5914283506847613,\n 1.8976990616650726,\n 1.758223085999124,\n 2.2294649594813816\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod2\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.0914263380328766,\n 1.8164545521094575,\n 1.621111084665713,\n 1.3902653996444705,\n 1.482803315949775\n ]\n ]\n }\n }\n]", + "scenarioId": "raw_frame" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-29": { + "kind": "Panel", + "spec": { + "id": 29, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar chart panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-30": { + "kind": "Panel", + "spec": { + "id": 30, + "title": "Flot graph - x axis histogram mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "histogram", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-31": { + "kind": "Panel", + "spec": { + "id": 31, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Histogram panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-32": { + "kind": "Panel", + "spec": { + "id": 32, + "title": "Flot graph - x axis series mode (with legend calcs)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "datasource", + "spec": { + "panelId": 28 + } + }, + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "bargauge", + "spec": { + "pluginVersion": "11.3.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-33": { + "kind": "Panel", + "spec": { + "id": 33, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.3.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar gauge panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Flot graph", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "11.0.0-pre", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Table (old) \u003e\u003e Table\n\nKnown issues:\n* wrapping text\n* style changes", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "grafana-singlestat-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 11, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-28" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 11, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-29" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-32" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-30" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 22, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-33" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 22, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-31" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 33, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 33, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-23" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 51, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-24" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 51, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-25" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 61, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-26" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 61, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-27" + } + } + } + ] + } + }, + "links": [ + { + "title": "Auto migrate graph panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate graph panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate table (old) panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate table (old) panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate piechart panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate piechart panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate worldmap panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate worldmap panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate stat panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate stat panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Disable angular (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.disableAngular=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Disable angular (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.disableAngular=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "gdev", + "migrations", + "angular" + ], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Devenv - Panel migrations", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2beta1.json new file mode 100644 index 00000000000..63447487dca --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.angular-migrations.v2beta1.json @@ -0,0 +1,1912 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "angular-migrations-test" + }, + "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": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "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-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table (old)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk_table" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "9.5.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "table-old", + "originalOptions": { + "columns": [], + "fontSize": "100%", + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "right", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "transform": "table" + } + }, + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-23": { + "kind": "Panel", + "spec": { + "id": 23, + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "11.0.0-pre", + "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-24": { + "kind": "Panel", + "spec": { + "id": 24, + "title": "grafana-piechart-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk_table" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "piechart", + "version": "9.5.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-piechart-panel", + "originalOptions": {} + }, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-25": { + "kind": "Panel", + "spec": { + "id": 25, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-piechart-panel \u003e\u003e piechart\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-26": { + "kind": "Panel", + "spec": { + "id": 26, + "title": "grafana-worldmap-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "csvFileName": "flight_info_by_state.csv", + "scenarioId": "csv_file" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "merge", + "spec": { + "id": "merge", + "options": { + "reducers": [] + } + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "sum" + ] + } + } + } + ], + "queryOptions": { + "maxDataPoints": 1 + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "geomap", + "version": "10.4.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-worldmap-panel", + "originalOptions": { + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 0, + "esMetric": "Count", + "hideEmpty": false, + "hideZero": false, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, + "mouseWheelZoom": false, + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, + "thresholds": "0,10", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" + } + }, + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-27": { + "kind": "Panel", + "spec": { + "id": 27, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# grafana-worldmap-panel \u003e\u003e geomap\n\nKnown issues:\n* TBD", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-28": { + "kind": "Panel", + "spec": { + "id": 28, + "title": "Flot graph - x axis series mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.260951647569786,\n 1.887442338051216,\n 2.1526144400893514,\n 1.7287721375237766,\n 1.7262902137793208\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod1\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 1.589539045095202,\n 1.5914283506847613,\n 1.8976990616650726,\n 1.758223085999124,\n 2.2294649594813816\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ],\n \"custom\": {\n \"customStat\": 10\n }\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 3600000\n }\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {\n \"pod\": \"A-pod2\"\n },\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1727107111901,\n 1727110711901,\n 1727114311901,\n 1727117911901,\n 1727121511901,\n 1727125111901\n ],\n [\n 1.907286825122581,\n 2.0914263380328766,\n 1.8164545521094575,\n 1.621111084665713,\n 1.3902653996444705,\n 1.482803315949775\n ]\n ]\n }\n }\n]", + "scenarioId": "raw_frame" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "11.0.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-29": { + "kind": "Panel", + "spec": { + "id": 29, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar chart panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-30": { + "kind": "Panel", + "spec": { + "id": 30, + "title": "Flot graph - x axis histogram mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "histogram", + "version": "11.0.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-31": { + "kind": "Panel", + "spec": { + "id": 31, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Histogram panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-32": { + "kind": "Panel", + "spec": { + "id": 32, + "title": "Flot graph - x axis series mode (with legend calcs)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "datasource", + "version": "v0", + "datasource": { + "name": "-- Dashboard --" + }, + "spec": { + "panelId": 28 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "bargauge", + "version": "11.3.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-33": { + "kind": "Panel", + "spec": { + "id": 33, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.3.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Bar gauge panel\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Flot graph", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "11.0.0-pre", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "fillGradient": 0, + "hiddenSeries": false, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeRegions": [], + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + }, + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "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": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "11.0.0-pre", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Table (old) \u003e\u003e Table\n\nKnown issues:\n* wrapping text\n* style changes", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "grafana-singlestat-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": { + "__angularMigration": { + "autoMigrateFrom": "grafana-singlestat-panel", + "originalOptions": { + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "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": "", + "thresholds": "", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 11, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-28" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 11, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-29" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-32" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 16, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-30" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 22, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-33" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 22, + "width": 8, + "height": 11, + "element": { + "kind": "ElementReference", + "name": "panel-31" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 33, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 33, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-23" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 51, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-24" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 51, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-25" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 61, + "width": 16, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-26" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 61, + "width": 8, + "height": 10, + "element": { + "kind": "ElementReference", + "name": "panel-27" + } + } + } + ] + } + }, + "links": [ + { + "title": "Auto migrate graph panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate graph panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateGraphPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate table (old) panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate table (old) panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateTablePanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate piechart panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate piechart panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigratePiechartPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate worldmap panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate worldmap panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateWorldmapPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate stat panel (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate stat panel (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": " /d/cdd412c4/?__feature.autoMigrateStatPanel=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Auto migrate (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.autoMigrateOldPanels=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Disable angular (TRUE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.disableAngular=true", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Disable angular (FALSE)", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "/d/cdd412c4/?__feature.disableAngular=false", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "gdev", + "migrations", + "angular" + ], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Devenv - Panel migrations", + "variables": [] + }, + "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 index 387ba145494..5f94f0d555b 100644 --- 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 @@ -335,5 +335,10 @@ "title": "Annotation Conversions Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json index a4dde4d26ac..c728d8dd513 100644 --- 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 @@ -346,5 +346,10 @@ "title": "Annotation Conversions Test Dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v0alpha1.json new file mode 100644 index 00000000000..449e76f1173 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v0alpha1.json @@ -0,0 +1,161 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "bom-in-links-test", + "namespace": "org-1", + "labels": { + "test": "bom-stripping" + } + }, + "spec": { + "description": "Testing that BOM characters are stripped from URLs during conversion", + "editable": true, + "links": [ + { + "icon": "external link", + "targetBlank": true, + "title": "Dashboard link with BOM", + "type": "link", + "url": "http://example.com?var=${datasource}\u0026other=value" + } + ], + "panels": [ + { + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "server" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Override link with BOM", + "url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "links": [ + { + "targetBlank": true, + "title": "Panel data link with BOM", + "url": "http://example.com/${__data.fields.cluster}\u0026var=value" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "test-ds" + }, + "refId": "A" + } + ], + "title": "Panel with BOM in field config override links", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Field config default link with BOM", + "url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}" + } + ] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Options data link with BOM", + "url": "http://example.com?series=${__series.name}\u0026time=${__value.time}" + } + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "test-ds" + }, + "refId": "A" + } + ], + "title": "Panel with BOM in options dataLinks", + "type": "timeseries" + } + ], + "schemaVersion": 42, + "tags": [ + "test", + "bom" + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m" + ] + }, + "title": "BOM Stripping 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.bom-in-links.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2alpha1.json new file mode 100644 index 00000000000..38547ea5b8e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2alpha1.json @@ -0,0 +1,242 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "bom-in-links-test", + "namespace": "org-1", + "labels": { + "test": "bom-stripping" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing that BOM characters are stripped from URLs during conversion", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with BOM in field config override links", + "description": "", + "links": [ + { + "title": "Panel data link with BOM", + "url": "http://example.com/${__data.fields.cluster}\u0026var=value", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "test-ds" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "server" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Override link with BOM", + "url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" + } + ] + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with BOM in options dataLinks", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "test-ds" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Options data link with BOM", + "url": "http://example.com?series=${__series.name}\u0026time=${__value.time}" + } + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Field config default link with BOM", + "url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}" + } + ] + }, + "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": [ + { + "title": "Dashboard link with BOM", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "http://example.com?var=${datasource}\u0026other=value", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "bom" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "BOM Stripping Test Dashboard", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2beta1.json new file mode 100644 index 00000000000..d85da89fe7a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2beta1.json @@ -0,0 +1,246 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "bom-in-links-test", + "namespace": "org-1", + "labels": { + "test": "bom-stripping" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing that BOM characters are stripped from URLs during conversion", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with BOM in field config override links", + "description": "", + "links": [ + { + "title": "Panel data link with BOM", + "url": "http://example.com/${__data.fields.cluster}\u0026var=value", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "test-ds" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "server" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Override link with BOM", + "url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}" + } + ] + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with BOM in options dataLinks", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "test-ds" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Options data link with BOM", + "url": "http://example.com?series=${__series.name}\u0026time=${__value.time}" + } + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Field config default link with BOM", + "url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}" + } + ] + }, + "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": [ + { + "title": "Dashboard link with BOM", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "http://example.com?var=${datasource}\u0026other=value", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "bom" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "BOM Stripping Test Dashboard", + "variables": [] + }, + "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 index 3e9132d9ea9..4de46378b53 100644 --- 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 @@ -200,5 +200,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2beta1.json index 951eeaac0f6..7576414d605 100644 --- 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 @@ -205,5 +205,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v0alpha1.json new file mode 100644 index 00000000000..463d1864dce --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v0alpha1.json @@ -0,0 +1,172 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "groupby-test" + }, + "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": { + "type": "prometheus", + "uid": "test-uid" + }, + "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": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "editorMode": "code", + "expr": "sum(counters_requests)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "works with group by var", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ], + "value": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "name": "Group by", + "type": "groupby" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "groupby test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2alpha1.json new file mode 100644 index 00000000000..58bf555354c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2alpha1.json @@ -0,0 +1,229 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "groupby-test" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "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": "works with group by var", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "editorMode": "code", + "expr": "sum(counters_requests)", + "legendFormat": "__auto", + "range": true + } + }, + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "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": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "groupby test", + "variables": [ + { + "kind": "GroupByVariable", + "spec": { + "name": "Group by", + "datasource": { + "type": "prometheus", + "uid": "test-uid" + }, + "current": { + "text": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ], + "value": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ] + }, + "options": [], + "multi": true, + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2beta1.json new file mode 100644 index 00000000000..c2ac16a874c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.groupby.v2beta1.json @@ -0,0 +1,232 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "groupby-test" + }, + "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": "works with group by var", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "test-uid" + }, + "spec": { + "editorMode": "code", + "expr": "sum(counters_requests)", + "legendFormat": "__auto", + "range": true + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "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": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "groupby test", + "variables": [ + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "test-uid" + }, + "spec": { + "name": "Group by", + "current": { + "text": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ], + "value": [ + "a_legacy_label", + "app", + "exported_instance", + "exported_job" + ] + }, + "options": [], + "multi": true, + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json index a614f32dda6..a9b876424be 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json @@ -165,5 +165,10 @@ "title": "Library Panel Repeat Options Test Dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json index d06d12848e6..8b2eb35cef1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json @@ -165,5 +165,10 @@ "title": "Library Panel Repeat Options Test Dashboard", "variables": [] }, - "status": {} + "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 index 9e0d5378f9b..f1004780fb0 100644 --- 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 @@ -181,7 +181,12 @@ "kind": "timeseries", "spec": { "pluginVersion": "", - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -501,5 +506,10 @@ "title": "Panel Conversions Test Dashboard", "variables": [] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2beta1.json index 8eac23d96c0..84064324429 100644 --- 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 @@ -186,7 +186,12 @@ "group": "timeseries", "version": "", "spec": { - "options": {}, + "options": { + "__angularMigration": { + "autoMigrateFrom": "graph", + "originalOptions": {} + } + }, "fieldConfig": { "defaults": {}, "overrides": [] @@ -510,5 +515,10 @@ "title": "Panel Conversions Test Dashboard", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json index 5a0a8e074e6..55b0af155ff 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json @@ -201,5 +201,10 @@ "title": "Panel ds inheritance ", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json index 3a13853c08e..48ec1ca758b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json @@ -204,5 +204,10 @@ "title": "Panel ds inheritance ", "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v0alpha1.json new file mode 100644 index 00000000000..ad063633f86 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v0alpha1.json @@ -0,0 +1,161 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "ad5vfcn", + "namespace": "default", + "uid": "dlMZZl6GndU8gJLUQSmgZxXBPCNXyXhNBeQJhHXl0r4X", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-11-28T10:14:21Z", + "labels": { + "grafana.app/deprecatedInternalID": "288" + }, + "annotations": { + "grafana.app/createdBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedTimestamp": "2025-11-28T10:15:06Z" + } + }, + "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, + "id": 288, + "links": [], + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "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, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": {}, + "queryType": "randomWalk" + }, + { + "datasource": {}, + "queryType": "randomWalk" + } + ], + "title": "New panel", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Panel ds inheritance ", + "uid": "ad5vfcn", + "version": 2 + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2alpha1.json new file mode 100644 index 00000000000..a8ce2b6a42b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2alpha1.json @@ -0,0 +1,227 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "ad5vfcn", + "namespace": "default", + "uid": "dlMZZl6GndU8gJLUQSmgZxXBPCNXyXhNBeQJhHXl0r4X", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-11-28T10:14:21Z", + "labels": { + "grafana.app/deprecatedInternalID": "288" + }, + "annotations": { + "grafana.app/createdBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedTimestamp": "2025-11-28T10:15:06Z" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "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": "New panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "testdata", + "spec": { + "queryType": "randomWalk" + } + }, + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "testdata", + "spec": { + "queryType": "randomWalk" + } + }, + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "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": "Panel ds inheritance ", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2beta1.json new file mode 100644 index 00000000000..c407d612154 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-target-missing-refid.v2beta1.json @@ -0,0 +1,231 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "ad5vfcn", + "namespace": "default", + "uid": "dlMZZl6GndU8gJLUQSmgZxXBPCNXyXhNBeQJhHXl0r4X", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-11-28T10:14:21Z", + "labels": { + "grafana.app/deprecatedInternalID": "288" + }, + "annotations": { + "grafana.app/createdBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedBy": "user:eex2ofwuj0agwd", + "grafana.app/updatedTimestamp": "2025-11-28T10:15:06Z" + } + }, + "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": "New panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "testdata", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "queryType": "randomWalk" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "testdata", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "queryType": "randomWalk" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.4.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "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": "Panel ds inheritance ", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v0alpha1.json new file mode 100644 index 00000000000..c824b19412d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v0alpha1.json @@ -0,0 +1,580 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "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": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "critical": { + "color": "red", + "index": 0, + "text": "Critical!" + }, + "ok": { + "color": "green", + "index": 2, + "text": "OK" + }, + "warning": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "targets": [ + { + "expr": "up", + "refId": "A" + } + ], + "title": "ValueMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0, + "text": "Low" + }, + "to": 50 + }, + "type": "range" + }, + { + "options": { + "from": 50, + "result": { + "color": "orange", + "index": 1, + "text": "Medium" + }, + "to": 80 + }, + "type": "range" + }, + { + "options": { + "from": 80, + "result": { + "color": "red", + "index": 2, + "text": "High" + }, + "to": 100 + }, + "type": "range" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "targets": [ + { + "expr": "cpu_usage_percent", + "refId": "A" + } + ], + "title": "RangeMap Example", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "pattern": "/^error.*/", + "result": { + "color": "red", + "index": 0, + "text": "Error" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^warn.*/", + "result": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^info.*/", + "result": { + "color": "blue", + "index": 2, + "text": "Info" + } + }, + "type": "regex" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "targets": [ + { + "expr": "log_level", + "refId": "A" + } + ], + "title": "RegexMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 0, + "text": "No Data" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "gray", + "index": 1, + "text": "Not a Number" + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "gray", + "index": 2, + "text": "N/A" + } + }, + "type": "special" + }, + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 3, + "text": "Yes" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 4, + "text": "No" + } + }, + "type": "special" + }, + { + "options": { + "match": "empty", + "result": { + "color": "gray", + "index": 5, + "text": "Empty" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "targets": [ + { + "expr": "some_metric", + "refId": "A" + } + ], + "title": "SpecialValueMap Example", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "failure": { + "color": "red", + "index": 1, + "text": "Failure" + }, + "success": { + "color": "green", + "index": 0, + "text": "Success" + } + }, + "type": "value" + }, + { + "options": { + "from": 0, + "result": { + "color": "blue", + "index": 2, + "text": "In Range" + }, + "to": 100 + }, + "type": "range" + }, + { + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "color": "purple", + "index": 3, + "text": "ID Format" + } + }, + "type": "regex" + }, + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 4, + "text": "Missing" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 5, + "targets": [ + { + "expr": "combined_metric", + "refId": "A" + }, + { + "expr": "secondary_metric", + "refId": "B" + } + ], + "title": "Combined Mappings and Overrides Example", + "type": "table" + } + ], + "schemaVersion": 42, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Value Mapping and Overrides Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2alpha1.json new file mode 100644 index 00000000000..f4a950d53a6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2alpha1.json @@ -0,0 +1,783 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "description": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "ValueMap Example", + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "critical": { + "text": "Critical!", + "color": "red", + "index": 0 + }, + "ok": { + "text": "OK", + "color": "green", + "index": 2 + }, + "warning": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "RangeMap Example", + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "cpu_usage_percent" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "range", + "options": { + "from": 0, + "to": 50, + "result": { + "text": "Low", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 80, + "result": { + "text": "Medium", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": 80, + "to": 100, + "result": { + "text": "High", + "color": "red", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "RegexMap Example", + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "log_level" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "/^error.*/", + "result": { + "text": "Error", + "color": "red", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^warn.*/", + "result": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^info.*/", + "result": { + "text": "Info", + "color": "blue", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "SpecialValueMap Example", + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "some_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "No Data", + "color": "gray", + "index": 0 + } + } + }, + { + "type": "special", + "options": { + "match": "nan", + "result": { + "text": "Not a Number", + "color": "gray", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "null+nan", + "result": { + "text": "N/A", + "color": "gray", + "index": 2 + } + } + }, + { + "type": "special", + "options": { + "match": "true", + "result": { + "text": "Yes", + "color": "green", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "false", + "result": { + "text": "No", + "color": "red", + "index": 4 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": "Empty", + "color": "gray", + "index": 5 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Combined Mappings and Overrides Example", + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "combined_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "secondary_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "failure": { + "text": "Failure", + "color": "red", + "index": 1 + }, + "success": { + "text": "Success", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 100, + "result": { + "text": "In Range", + "color": "blue", + "index": 2 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "text": "ID Format", + "color": "purple", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Missing", + "color": "gray", + "index": 4 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": 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": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "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": "Value Mapping and Overrides Test", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2beta1.json new file mode 100644 index 00000000000..ad492e24a6e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.value-mapping-and-overrides.v2beta1.json @@ -0,0 +1,795 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "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": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "ValueMap Example", + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "up" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "critical": { + "text": "Critical!", + "color": "red", + "index": 0 + }, + "ok": { + "text": "OK", + "color": "green", + "index": 2 + }, + "warning": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "RangeMap Example", + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "cpu_usage_percent" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "range", + "options": { + "from": 0, + "to": 50, + "result": { + "text": "Low", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 80, + "result": { + "text": "Medium", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": 80, + "to": 100, + "result": { + "text": "High", + "color": "red", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "RegexMap Example", + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "log_level" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "/^error.*/", + "result": { + "text": "Error", + "color": "red", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^warn.*/", + "result": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^info.*/", + "result": { + "text": "Info", + "color": "blue", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "SpecialValueMap Example", + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "some_metric" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "No Data", + "color": "gray", + "index": 0 + } + } + }, + { + "type": "special", + "options": { + "match": "nan", + "result": { + "text": "Not a Number", + "color": "gray", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "null+nan", + "result": { + "text": "N/A", + "color": "gray", + "index": 2 + } + } + }, + { + "type": "special", + "options": { + "match": "true", + "result": { + "text": "Yes", + "color": "green", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "false", + "result": { + "text": "No", + "color": "red", + "index": 4 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": "Empty", + "color": "gray", + "index": 5 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Combined Mappings and Overrides Example", + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "combined_metric" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus-uid" + }, + "spec": { + "expr": "secondary_metric" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "failure": { + "text": "Failure", + "color": "red", + "index": 1 + }, + "success": { + "text": "Success", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 100, + "result": { + "text": "In Range", + "color": "blue", + "index": 2 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "text": "ID Format", + "color": "purple", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Missing", + "color": "gray", + "index": 4 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": 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": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "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": "Value Mapping and Overrides Test", + "variables": [] + }, + "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.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index 4e12e6982ef..725e658dfc2 100644 --- 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 @@ -94,6 +94,7 @@ "query": "label_values(up, instance)", "refresh": 2, "regex": "/.*9090.*/", + "regexApplyTo": "text", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", @@ -362,6 +363,7 @@ }, "refresh": 1, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "skipUrlSync": false, "sort": 2, "tagValuesQuery": "", 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 index c7c6c646a94..040513a3ce5 100644 --- 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 @@ -110,6 +110,7 @@ } }, "regex": "/.*9090.*/", + "regexApplyTo": "text", "sort": "alphabeticalAsc", "definition": "label_values(up, instance)", "options": [ @@ -401,6 +402,7 @@ } }, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "sort": "alphabeticalDesc", "definition": "terms field:@host size:100", "options": [], @@ -412,5 +414,10 @@ } ] }, - "status": {} + "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.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index 7b1a899d7fa..cf3ff5443c0 100644 --- 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 @@ -111,6 +111,7 @@ } }, "regex": "/.*9090.*/", + "regexApplyTo": "text", "sort": "alphabeticalAsc", "definition": "label_values(up, instance)", "options": [ @@ -404,6 +405,7 @@ } }, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "sort": "alphabeticalDesc", "definition": "terms field:@host size:100", "options": [], @@ -415,5 +417,10 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v0alpha1.json new file mode 100644 index 00000000000..0357c22536a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v0alpha1.json @@ -0,0 +1,511 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "adt885j", + "namespace": "default", + "uid": "yTWet6JgBjlRIWnqRE9ZOmUycfT0tEkr2mljaln1GWIX", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-12-16T10:44:31Z", + "labels": { + "grafana.app/deprecatedInternalID": "2409" + }, + "annotations": { + "grafana.app/createdBy": "user:u000000001", + "grafana.app/updatedBy": "user:u000000001", + "grafana.app/updatedTimestamp": "2025-12-16T10:51:14Z" + } + }, + "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": "", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": -1, + "panels": [], + "title": "Tab1", + "type": "row" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Panel1", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel2", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 15, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel3", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel4", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel5", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Dashboard with tabs" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v1beta1.json new file mode 100644 index 00000000000..7a9ea77ba65 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v1beta1.json @@ -0,0 +1,511 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "adt885j", + "namespace": "default", + "uid": "yTWet6JgBjlRIWnqRE9ZOmUycfT0tEkr2mljaln1GWIX", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-12-16T10:44:31Z", + "labels": { + "grafana.app/deprecatedInternalID": "2409" + }, + "annotations": { + "grafana.app/createdBy": "user:u000000001", + "grafana.app/updatedBy": "user:u000000001", + "grafana.app/updatedTimestamp": "2025-12-16T10:51:14Z" + } + }, + "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": "", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": -1, + "panels": [], + "title": "Tab1", + "type": "row" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Panel1", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel2", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 15, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel3", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel4", + "type": "timeseries" + }, + { + "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" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0-pre", + "targets": [ + { + "refId": "A" + } + ], + "title": "Panel5", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Dashboard with tabs" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v2alpha1.json new file mode 100644 index 00000000000..b7739508c2d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.tab-with-multiple-panels.v2alpha1.json @@ -0,0 +1,683 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "adt885j", + "namespace": "default", + "uid": "yTWet6JgBjlRIWnqRE9ZOmUycfT0tEkr2mljaln1GWIX", + "resourceVersion": "2", + "generation": 2, + "creationTimestamp": "2025-12-16T10:44:31Z", + "labels": { + "grafana.app/deprecatedInternalID": "2409" + }, + "annotations": { + "grafana.app/createdBy": "user:u000000001", + "grafana.app/updatedBy": "user:u000000001", + "grafana.app/updatedTimestamp": "2025-12-16T10:51:14Z" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel1", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel2", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel3", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel4", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel5", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.4.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "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" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "TabsLayout", + "spec": { + "tabs": [ + { + "kind": "TabsLayoutTab", + "spec": { + "title": "Tab1", + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 7, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 7, + "y": 0, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 15, + "y": 0, + "width": 9, + "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" + } + } + } + ] + } + } + } + } + ] + } + }, + "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 tabs", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v0alpha1.json new file mode 100644 index 00000000000..bd8e440efb2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v0alpha1.json @@ -0,0 +1,640 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "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": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "liveNow": false, + "panels": [ + { + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "critical": { + "color": "red", + "index": 0, + "text": "Critical!" + }, + "ok": { + "color": "green", + "index": 2, + "text": "OK" + }, + "warning": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "ValueMap Example", + "type": "stat" + }, + { + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0, + "text": "Low" + }, + "to": 50 + }, + "type": "range" + }, + { + "options": { + "from": 50, + "result": { + "color": "orange", + "index": 1, + "text": "Medium" + }, + "to": 80 + }, + "type": "range" + }, + { + "options": { + "from": 80, + "result": { + "color": "red", + "index": 2, + "text": "High" + }, + "to": 100 + }, + "type": "range" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "cpu_usage_percent", + "refId": "A" + } + ], + "title": "RangeMap Example", + "type": "gauge" + }, + { + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "pattern": "/^error.*/", + "result": { + "color": "red", + "index": 0, + "text": "Error" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^warn.*/", + "result": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^info.*/", + "result": { + "color": "blue", + "index": 2, + "text": "Info" + } + }, + "type": "regex" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "log_level", + "refId": "A" + } + ], + "title": "RegexMap Example", + "type": "stat" + }, + { + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 0, + "text": "No Data" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "gray", + "index": 1, + "text": "Not a Number" + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "gray", + "index": 2, + "text": "N/A" + } + }, + "type": "special" + }, + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 3, + "text": "Yes" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 4, + "text": "No" + } + }, + "type": "special" + }, + { + "options": { + "match": "empty", + "result": { + "color": "gray", + "index": 5, + "text": "Empty" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "some_metric", + "refId": "A" + } + ], + "title": "SpecialValueMap Example", + "type": "stat" + }, + { + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "failure": { + "color": "red", + "index": 1, + "text": "Failure" + }, + "success": { + "color": "green", + "index": 0, + "text": "Success" + } + }, + "type": "value" + }, + { + "options": { + "from": 0, + "result": { + "color": "blue", + "index": 2, + "text": "In Range" + }, + "to": 100 + }, + "type": "range" + }, + { + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "color": "purple", + "index": 3, + "text": "ID Format" + } + }, + "type": "regex" + }, + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 4, + "text": "Missing" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 5, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "combined_metric", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "secondary_metric", + "refId": "B" + } + ], + "title": "Combined Mappings and Overrides Example", + "type": "table" + }, + { + "description": "Panel with override that has empty properties array - tests conversion of overrides without any property modifications", + "fieldConfig": { + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "field_with_empty_override" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 6, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "empty_override_metric", + "refId": "A" + } + ], + "title": "Empty Properties Override Example", + "type": "stat" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Value Mapping and Overrides Test" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v1beta1.json new file mode 100644 index 00000000000..07d899db46c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v1beta1.json @@ -0,0 +1,640 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "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": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "liveNow": false, + "panels": [ + { + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "critical": { + "color": "red", + "index": 0, + "text": "Critical!" + }, + "ok": { + "color": "green", + "index": 2, + "text": "OK" + }, + "warning": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "ValueMap Example", + "type": "stat" + }, + { + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0, + "text": "Low" + }, + "to": 50 + }, + "type": "range" + }, + { + "options": { + "from": 50, + "result": { + "color": "orange", + "index": 1, + "text": "Medium" + }, + "to": 80 + }, + "type": "range" + }, + { + "options": { + "from": 80, + "result": { + "color": "red", + "index": 2, + "text": "High" + }, + "to": 100 + }, + "type": "range" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "cpu_usage_percent", + "refId": "A" + } + ], + "title": "RangeMap Example", + "type": "gauge" + }, + { + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "pattern": "/^error.*/", + "result": { + "color": "red", + "index": 0, + "text": "Error" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^warn.*/", + "result": { + "color": "orange", + "index": 1, + "text": "Warning" + } + }, + "type": "regex" + }, + { + "options": { + "pattern": "/^info.*/", + "result": { + "color": "blue", + "index": 2, + "text": "Info" + } + }, + "type": "regex" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "log_level", + "refId": "A" + } + ], + "title": "RegexMap Example", + "type": "stat" + }, + { + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 0, + "text": "No Data" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "gray", + "index": 1, + "text": "Not a Number" + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "gray", + "index": 2, + "text": "N/A" + } + }, + "type": "special" + }, + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 3, + "text": "Yes" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 4, + "text": "No" + } + }, + "type": "special" + }, + { + "options": { + "match": "empty", + "result": { + "color": "gray", + "index": 5, + "text": "Empty" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "some_metric", + "refId": "A" + } + ], + "title": "SpecialValueMap Example", + "type": "stat" + }, + { + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "failure": { + "color": "red", + "index": 1, + "text": "Failure" + }, + "success": { + "color": "green", + "index": 0, + "text": "Success" + } + }, + "type": "value" + }, + { + "options": { + "from": 0, + "result": { + "color": "blue", + "index": 2, + "text": "In Range" + }, + "to": 100 + }, + "type": "range" + }, + { + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "color": "purple", + "index": 3, + "text": "ID Format" + } + }, + "type": "regex" + }, + { + "options": { + "match": "null", + "result": { + "color": "gray", + "index": 4, + "text": "Missing" + } + }, + "type": "special" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 5, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "combined_metric", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "secondary_metric", + "refId": "B" + } + ], + "title": "Combined Mappings and Overrides Example", + "type": "table" + }, + { + "description": "Panel with override that has empty properties array - tests conversion of overrides without any property modifications", + "fieldConfig": { + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "field_with_empty_override" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 6, + "options": {}, + "pluginVersion": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "expr": "empty_override_metric", + "refId": "A" + } + ], + "title": "Empty Properties Override Example", + "type": "stat" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "Value Mapping and Overrides Test" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v2alpha1.json new file mode 100644 index 00000000000..425b914eafb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.value-mapping-and-overrides.v2alpha1.json @@ -0,0 +1,850 @@ +{ + "kind": "DashboardWithAccessInfo", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "value-mapping-test", + "namespace": "default", + "uid": "value-mapping-test", + "resourceVersion": "1765384157199094", + "generation": 2, + "creationTimestamp": "2025-11-19T20:09:28Z", + "labels": { + "grafana.app/deprecatedInternalID": "646372978987008" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "description": "Test dashboard for all value mapping types and override matcher types", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "ValueMap Example", + "description": "Panel with ValueMap mapping type - maps specific text values to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "critical": { + "text": "Critical!", + "color": "red", + "index": 0 + }, + "ok": { + "text": "OK", + "color": "green", + "index": 2 + }, + "warning": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "RangeMap Example", + "description": "Panel with RangeMap mapping type - maps numerical ranges to colors and display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "cpu_usage_percent" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "range", + "options": { + "from": 0, + "to": 50, + "result": { + "text": "Low", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 80, + "result": { + "text": "Medium", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": 80, + "to": 100, + "result": { + "text": "High", + "color": "red", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/^cpu_/" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "RegexMap Example", + "description": "Panel with RegexMap mapping type - maps values matching regex patterns to colors", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "log_level" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "/^error.*/", + "result": { + "text": "Error", + "color": "red", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^warn.*/", + "result": { + "text": "Warning", + "color": "orange", + "index": 1 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^info.*/", + "result": { + "text": "Info", + "color": "blue", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "string" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "SpecialValueMap Example", + "description": "Panel with SpecialValueMap mapping type - maps special values like null, NaN, true, false to display text", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "some_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "No Data", + "color": "gray", + "index": 0 + } + } + }, + { + "type": "special", + "options": { + "match": "nan", + "result": { + "text": "Not a Number", + "color": "gray", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "null+nan", + "result": { + "text": "N/A", + "color": "gray", + "index": 2 + } + } + }, + { + "type": "special", + "options": { + "match": "true", + "result": { + "text": "Yes", + "color": "green", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "false", + "result": { + "text": "No", + "color": "red", + "index": 4 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": "Empty", + "color": "gray", + "index": 5 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Combined Mappings and Overrides Example", + "description": "Panel with all mapping types combined - demonstrates mixing different mapping types and multiple override matchers", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "combined_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "secondary_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "failure": { + "text": "Failure", + "color": "red", + "index": 1 + }, + "success": { + "text": "Success", + "color": "green", + "index": 0 + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 100, + "result": { + "text": "In Range", + "color": "blue", + "index": 2 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "/^[A-Z]{3}-\\d+$/", + "result": { + "text": "ID Format", + "color": "purple", + "index": 3 + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Missing", + "color": "gray", + "index": 4 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/^value_/" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "min", + "value": 0 + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "number" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "Secondary Query" + } + ] + }, + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + } + ] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Empty Properties Override Example", + "description": "Panel with override that has empty properties array - tests conversion of overrides without any property modifications", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "empty_override_metric" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "field_with_empty_override" + }, + "properties": [] + } + ] + } + } + } + } + } + }, + "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": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 24, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "value-mapping", + "overrides", + "test" + ], + "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": "Value Mapping and Overrides Test", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index 826293d9564..513f6975516 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -13,6 +13,9 @@ import ( 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.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv1.APIVERSION + out.Kind = in.Kind out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -22,12 +25,24 @@ func Convert_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope co } return err } + + // Set successful conversion status + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: false, + }, + } + return nil } func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2alpha1.APIVERSION + out.Kind = in.Kind out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -35,7 +50,6 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s 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{ @@ -45,10 +59,13 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s }, } } - return nil + return err } if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider, leIndexProvider); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2alpha1.APIVERSION + out.Kind = in.Kind out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -56,7 +73,6 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s 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{ @@ -66,7 +82,15 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s }, } } - return nil + return err + } + + // Set successful conversion status + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: false, + }, } return nil @@ -75,6 +99,9 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.APIVERSION + out.Kind = in.Kind out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -82,11 +109,23 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco Error: ptr.To(err.Error()), }, } + // 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 = dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + } + } return err } v2alpha1 := &dashv2alpha1.Dashboard{} if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.APIVERSION + out.Kind = in.Kind out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -94,10 +133,22 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco Error: ptr.To(err.Error()), }, } + // 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 = dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + } + } return err } if err := ConvertDashboard_V2alpha1_to_V2beta1(v2alpha1, out, scope); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.APIVERSION + out.Kind = in.Kind out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -105,8 +156,25 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco Error: ptr.To(err.Error()), }, } + // 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 = dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + } + } return err } + // Set successful conversion status + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: false, + }, + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v0_test.go b/apps/dashboard/pkg/migration/conversion/v0_test.go index ca2210f01e8..3620ea4d099 100644 --- a/apps/dashboard/pkg/migration/conversion/v0_test.go +++ b/apps/dashboard/pkg/migration/conversion/v0_test.go @@ -20,7 +20,7 @@ func TestV0ConversionErrorHandling(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) tests := []struct { name string @@ -54,7 +54,7 @@ func TestV0ConversionErrorHandling(t *testing.T) { }, }, { - name: "Convert_V0_to_V2alpha1 sets status on first step migration failure", + name: "Convert_V0_to_V2alpha1 returns error and sets status on first step migration failure", source: &dashv0.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Namespace: "org-1", @@ -67,7 +67,7 @@ func TestV0ConversionErrorHandling(t *testing.T) { }, }, target: &dashv2alpha1.Dashboard{}, - expectError: false, // Convert_V0_to_V2alpha1 doesn't return error, just sets status + expectError: true, // Convert_V0_to_V2alpha1 now returns error for proper metrics/logging expectStatusSet: true, checkStatus: func(t *testing.T, target interface{}) { out := target.(*dashv2alpha1.Dashboard) @@ -132,7 +132,7 @@ func TestV0ConversionErrorPropagation(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("ConvertDashboard_V0_to_V1beta1 returns error on migration failure", func(t *testing.T) { source := &dashv0.Dashboard{ @@ -206,7 +206,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V0_to_V1beta1 success path returns nil", func(t *testing.T) { source := &dashv0.Dashboard{ @@ -275,9 +275,9 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) - t.Run("Convert_V0_to_V2alpha1 sets status on first step error", func(t *testing.T) { + t.Run("Convert_V0_to_V2alpha1 returns error and 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{ @@ -295,16 +295,16 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) - // 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 + // Convert_V0_to_V2alpha1 returns error for proper metrics/logging + require.Error(t, err, "Convert_V0_to_V2alpha1 should return error") + // Status should also 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) { + t.Run("Convert_V0_to_V2alpha1 returns error and 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 @@ -329,16 +329,13 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) - // 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) - } + // Convert_V0_to_V2alpha1 returns error for proper metrics/logging + require.Error(t, err, "Convert_V0_to_V2alpha1 should return error") + // Status should also be set + require.NotNil(t, target.Status.Conversion, "Status should be set on 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_V2beta1 returns error on second step failure", func(t *testing.T) { @@ -394,3 +391,177 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } }) } + +// TestV0ConversionConsistency_ErrorsMustBeReturned ensures v0 conversion functions +// return errors instead of swallowing them. This prevents metrics and logs from being silently dropped. +func TestV0ConversionConsistency_ErrorsMustBeReturned(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Create a v0 dashboard that will fail conversion (invalid schema version) + invalidV0 := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version causes migration to fail + }, + }, + } + + t.Run("Convert_V0_to_V1beta1 must return error on failure", func(t *testing.T) { + target := &dashv1.Dashboard{} + err := Convert_V0_to_V1beta1(invalidV0, target, nil) + require.Error(t, err, "Convert_V0_to_V1beta1 must return error, not swallow it") + require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true") + }) + + t.Run("Convert_V0_to_V2alpha1 must return error on failure", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + err := Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider) + require.Error(t, err, "Convert_V0_to_V2alpha1 must return error, not swallow it") + require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true") + }) + + t.Run("Convert_V0_to_V2beta1 must return error on failure", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + err := Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider) + require.Error(t, err, "Convert_V0_to_V2beta1 must return error, not swallow it") + require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true") + }) +} + +// TestV0ConversionConsistency_SuccessStatusMustBeSet ensures v0 conversion functions +// set Status.Conversion with Failed=false on successful conversion. +func TestV0ConversionConsistency_SuccessStatusMustBeSet(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Create a valid v0 dashboard + validV0 := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, + }, + }, + } + + t.Run("Convert_V0_to_V1beta1 must set success status", func(t *testing.T) { + target := &dashv1.Dashboard{} + err := Convert_V0_to_V1beta1(validV0, target, nil) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion) + }) + + t.Run("Convert_V0_to_V2alpha1 must set success status", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + err := Convert_V0_to_V2alpha1(validV0, target, nil, dsProvider, leProvider) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion) + }) + + t.Run("Convert_V0_to_V2beta1 must set success status", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + err := Convert_V0_to_V2beta1(validV0, target, nil, dsProvider, leProvider) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion) + }) +} + +// TestV0ConversionConsistency_ObjectMetaMustBeSetOnError ensures v0 conversion functions +// set ObjectMeta, APIVersion, and Kind on the target even when conversion fails. +func TestV0ConversionConsistency_ObjectMetaMustBeSetOnError(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + invalidV0 := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + UID: "test-uid", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, + }, + }, + } + + t.Run("Convert_V0_to_V1beta1 must set ObjectMeta on error", func(t *testing.T) { + target := &dashv1.Dashboard{} + _ = Convert_V0_to_V1beta1(invalidV0, target, nil) + require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error") + require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error") + require.Equal(t, dashv1.APIVERSION, target.APIVersion, "APIVersion must be set on error") + }) + + t.Run("Convert_V0_to_V2alpha1 must set ObjectMeta on error", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + _ = Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider) + require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error") + require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error") + require.Equal(t, dashv2alpha1.APIVERSION, target.APIVersion, "APIVersion must be set on error") + }) + + t.Run("Convert_V0_to_V2beta1 must set ObjectMeta on error", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + _ = Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider) + require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error") + require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error") + require.Equal(t, dashv2beta1.APIVERSION, target.APIVersion, "APIVersion must be set on error") + }) +} + +// TestV0ConversionConsistency_LayoutMustBeSetOnError ensures v2alpha1 and v2beta1 targets +// have a default layout set even when conversion fails to prevent JSON marshaling errors. +func TestV0ConversionConsistency_LayoutMustBeSetOnError(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + invalidV0 := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, + }, + }, + } + + t.Run("Convert_V0_to_V2alpha1 must set default layout on error", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + _ = Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider) + require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues") + }) + + t.Run("Convert_V0_to_V2beta1 must set default layout on error", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + _ = Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider) + require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues") + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go index 2154a26e3a2..d0679c76e63 100644 --- a/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go @@ -2,6 +2,7 @@ package conversion import ( "context" + "strings" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apiserver/pkg/endpoints/request" @@ -79,5 +80,57 @@ func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1") } + // Normalize template variable datasources from string to object format + // This handles legacy dashboards where query variables have datasource: "$datasource" (string) + // instead of datasource: { uid: "$datasource" } (object) + // our migration pipeline in v36 doesn't address because this was not addressed historically + // in DashboardMigrator - see public/app/features/dashboard/state/DashboardMigrator.ts#L607 + // Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings + normalizeTemplateVariableDatasources(out.Spec.Object) + return nil } + +// normalizeTemplateVariableDatasources converts template variable string datasources to object format. +// Legacy dashboards may have query variables with datasource: "$datasource" (string). +// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion. +func normalizeTemplateVariableDatasources(dashboard map[string]interface{}) { + templating, ok := dashboard["templating"].(map[string]interface{}) + if !ok { + return + } + + list, ok := templating["list"].([]interface{}) + if !ok { + return + } + + for _, variable := range list { + varMap, ok := variable.(map[string]interface{}) + if !ok { + continue + } + + varType, _ := varMap["type"].(string) + if varType != "query" { + continue + } + + ds := varMap["datasource"] + if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) { + // Convert string template variable reference to object format + varMap["datasource"] = map[string]interface{}{ + "uid": dsStr, + } + } + } +} + +// isTemplateVariableRef checks if a string is a Grafana template variable reference. +// Template variables can be in the form: $varname or ${varname} +func isTemplateVariableRef(s string) bool { + if s == "" { + return false + } + return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${") +} diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index a6f2836b464..ac538e01e02 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -29,6 +29,9 @@ func Convert_V1beta1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope co func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider, leIndexProvider); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2alpha1.APIVERSION + out.Kind = in.Kind out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), @@ -36,7 +39,6 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa 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{ @@ -46,7 +48,7 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa }, } } - return nil + return err } // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. @@ -59,12 +61,23 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa } } + // Set successful conversion status + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv1.VERSION), + Failed: false, + }, + } + return nil } func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v2alpha1 := &dashv2alpha1.Dashboard{} if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.APIVERSION + out.Kind = in.Kind out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), @@ -72,10 +85,22 @@ func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard Error: ptr.To(err.Error()), }, } + // 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 = dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + } + } return err } if err := ConvertDashboard_V2alpha1_to_V2beta1(v2alpha1, out, scope); err != nil { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2beta1.APIVERSION + out.Kind = in.Kind out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), @@ -83,8 +108,25 @@ func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard Error: ptr.To(err.Error()), }, } + // 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 = dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + } + } return err } + // Set successful conversion status + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv1.VERSION), + Failed: false, + }, + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v1_test.go b/apps/dashboard/pkg/migration/conversion/v1_test.go index 4525e974e01..6eca186d3ee 100644 --- a/apps/dashboard/pkg/migration/conversion/v1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1_test.go @@ -19,11 +19,10 @@ func TestV1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) - 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 + t.Run("Convert_V1beta1_to_V2alpha1 sets status on successful conversion", func(t *testing.T) { + // Create a simple dashboard that will convert successfully source := &dashv1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Namespace: "org-1", @@ -31,7 +30,6 @@ func TestV1ConversionErrorHandling(t *testing.T) { Spec: common.Unstructured{ Object: map[string]interface{}{ "title": "test dashboard", - // Missing required fields that might cause conversion to fail }, }, } @@ -39,15 +37,14 @@ func TestV1ConversionErrorHandling(t *testing.T) { err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider, leProvider) - // Convert_V1beta1_to_V2alpha1 doesn't return error, just sets status - require.NoError(t, err, "Convert_V1beta1_to_V2alpha1 doesn't return error") + // Conversion should succeed + require.NoError(t, err) // 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) - } + // Status should be set with success + require.NotNil(t, target.Status.Conversion) + require.False(t, target.Status.Conversion.Failed) + require.Equal(t, dashv1.VERSION, *target.Status.Conversion.StoredVersion) }) t.Run("Convert_V1beta1_to_V2beta1 returns error on first step failure", func(t *testing.T) { @@ -126,3 +123,156 @@ func TestV1ConversionErrorHandling(t *testing.T) { } }) } + +// TestV1ConversionConsistency_ErrorsMustBeReturned ensures v1 conversion functions +// return errors instead of swallowing them. This prevents metrics and logs from being silently dropped. +func TestV1ConversionConsistency_ErrorsMustBeReturned(t *testing.T) { + leProvider := migrationtestutil.NewLibraryElementProvider() + + // Create a valid v1 dashboard - the nil dsProvider will cause conversion to fail + validV1 := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, + }, + }, + } + + t.Run("Convert_V1beta1_to_V2alpha1 must return error on failure", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2alpha1(validV1, target, nil, nil, leProvider) + require.Error(t, err, "Convert_V1beta1_to_V2alpha1 must return error, not swallow it") + require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true") + }) + + t.Run("Convert_V1beta1_to_V2beta1 must return error on failure", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2beta1(validV1, target, nil, nil, leProvider) + require.Error(t, err, "Convert_V1beta1_to_V2beta1 must return error, not swallow it") + require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true") + }) +} + +// TestV1ConversionConsistency_SuccessStatusMustBeSet ensures v1 conversion functions +// set Status.Conversion with Failed=false on successful conversion. +func TestV1ConversionConsistency_SuccessStatusMustBeSet(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Create a valid v1 dashboard + validV1 := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, + }, + }, + } + + t.Run("Convert_V1beta1_to_V2alpha1 must set success status", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + err := Convert_V1beta1_to_V2alpha1(validV1, target, nil, dsProvider, leProvider) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + require.Equal(t, dashv1.VERSION, *target.Status.Conversion.StoredVersion) + }) + + t.Run("Convert_V1beta1_to_V2beta1 must set success status", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + err := Convert_V1beta1_to_V2beta1(validV1, target, nil, dsProvider, leProvider) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + require.Equal(t, dashv1.VERSION, *target.Status.Conversion.StoredVersion) + }) +} + +// TestV1ConversionConsistency_ObjectMetaMustBeSetOnError ensures v1 conversion functions +// set ObjectMeta, APIVersion, and Kind on the target even when conversion fails. +func TestV1ConversionConsistency_ObjectMetaMustBeSetOnError(t *testing.T) { + leProvider := migrationtestutil.NewLibraryElementProvider() + + validV1 := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + UID: "test-uid", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, + }, + }, + } + + t.Run("Convert_V1beta1_to_V2alpha1 must set ObjectMeta on error", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2alpha1(validV1, target, nil, nil, leProvider) + require.Error(t, err) + require.Equal(t, validV1.Name, target.Name, "Name must be set on error") + require.Equal(t, validV1.Namespace, target.Namespace, "Namespace must be set on error") + require.Equal(t, dashv2alpha1.APIVERSION, target.APIVersion, "APIVersion must be set on error") + }) + + t.Run("Convert_V1beta1_to_V2beta1 must set ObjectMeta on error", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2beta1(validV1, target, nil, nil, leProvider) + require.Error(t, err) + require.Equal(t, validV1.Name, target.Name, "Name must be set on error") + require.Equal(t, validV1.Namespace, target.Namespace, "Namespace must be set on error") + require.Equal(t, dashv2beta1.APIVERSION, target.APIVersion, "APIVersion must be set on error") + }) +} + +// TestV1ConversionConsistency_LayoutMustBeSetOnError ensures v2alpha1 and v2beta1 targets +// have a default layout set even when conversion fails to prevent JSON marshaling errors. +func TestV1ConversionConsistency_LayoutMustBeSetOnError(t *testing.T) { + leProvider := migrationtestutil.NewLibraryElementProvider() + + validV1 := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "test-dashboard", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, + }, + }, + } + + t.Run("Convert_V1beta1_to_V2alpha1 must set default layout on error", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2alpha1(validV1, target, nil, nil, leProvider) + require.Error(t, err) + require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues") + }) + + t.Run("Convert_V1beta1_to_V2beta1 must set default layout on error", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + // Pass nil dsProvider to cause conversion to fail + err := Convert_V1beta1_to_V2beta1(validV1, target, nil, nil, leProvider) + require.Error(t, err) + require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues") + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 236a4337efb..b63e0146cc2 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -229,6 +229,46 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool return defaultValue } +// stripBOM removes Byte Order Mark (BOM) characters from a string. +// BOMs (U+FEFF) can be introduced through copy/paste from certain editors +// and cause CUE validation errors ("illegal byte order mark"). +func stripBOM(s string) string { + return strings.ReplaceAll(s, "\ufeff", "") +} + +// stripBOMFromInterface recursively strips BOM characters from all strings +// in an interface{} value (map, slice, or string). +func stripBOMFromInterface(v interface{}) interface{} { + switch val := v.(type) { + case string: + return stripBOM(val) + case map[string]interface{}: + result := make(map[string]interface{}, len(val)) + for k, v := range val { + result[k] = stripBOMFromInterface(v) + } + return result + case []interface{}: + result := make([]interface{}, len(val)) + for i, item := range val { + result[i] = stripBOMFromInterface(item) + } + return result + default: + return v + } +} + +func getUnionField[T ~string](m map[string]interface{}, key string) *T { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok && str != "" { + result := T(str) + return &result + } + } + return nil +} + // Helper function to create int64 pointer func int64Ptr(i int64) *int64 { return &i @@ -383,7 +423,8 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa // Optional field - only set if present if url, exists := linkMap["url"]; exists { if urlStr, ok := url.(string); ok { - dashLink.Url = &urlStr + cleanUrl := stripBOM(urlStr) + dashLink.Url = &cleanUrl } } @@ -491,11 +532,9 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi 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 + // it should not be collapsed because it will disappear and be visible only in edit mode if currentRow.Spec.HideHeader != nil && *currentRow.Spec.HideHeader { - rowCollapsed := getBoolField(panelMap, "collapsed", false) - currentRow.Spec.Collapse = &rowCollapsed + currentRow.Spec.Collapse = &[]bool{false}[0] } // Flush current row to layout rows = append(rows, *currentRow) @@ -1177,6 +1216,10 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm // If no UID and no type, use default datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } + } else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) { + // Handle datasource variable reference (e.g., "$datasource") + // Only process template variables - other string values are not supported in V2 format + datasourceUID = dsStr } else { datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } @@ -1195,6 +1238,7 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm Refresh: transformVariableRefreshToEnum(varMap["refresh"]), Sort: transformVariableSortToEnum(varMap["sort"]), Regex: schemaversion.GetStringValue(varMap, "regex"), + RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"), Query: buildDataQueryKindForVariable(varMap["query"], datasourceType), AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), }, @@ -1523,6 +1567,10 @@ func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, comm // If no UID and no type, use default datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } + } else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) { + // Handle datasource variable reference (e.g., "$datasource") + // Only process template variables - other string values are not supported in V2 format + datasourceUID = dsStr } else { datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } @@ -1700,6 +1748,10 @@ func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, co // Resolve Grafana datasource UID when type is "datasource" and UID is empty datasourceUID = resolveGrafanaDatasourceUID(datasourceType, datasourceUID) + } else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) { + // Handle datasource variable reference (e.g., "$datasource") + // Only process template variables - other string values are not supported in V2 format + datasourceUID = dsStr } else { datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } @@ -1713,7 +1765,9 @@ func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, co Hide: commonProps.Hide, SkipUrlSync: commonProps.SkipUrlSync, Current: buildVariableCurrent(varMap["current"]), - Multi: getBoolField(varMap, "multi", false), + // We set it to true by default because GroupByVariable + // constructor defaults to multi: true + Multi: getBoolField(varMap, "multi", true), }, } @@ -1997,6 +2051,28 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, } } + // Ensure each target has a non-empty refId. We only fill missing refIds; + existingRefIds := make(map[string]bool) + for _, target := range targets { + if targetMap, ok := target.(map[string]interface{}); ok { + if refId := schemaversion.GetStringValue(targetMap, "refId"); refId != "" { + existingRefIds[refId] = true + } + } + } + for _, target := range targets { + targetMap, ok := target.(map[string]interface{}) + if !ok { + continue + } + refId := schemaversion.GetStringValue(targetMap, "refId") + if refId == "" { + refId = nextAvailableRefId(existingRefIds) + targetMap["refId"] = refId + existingRefIds[refId] = true + } + } + queries := make([]dashv2alpha1.DashboardPanelQueryKind, 0, len(targets)) for _, target := range targets { @@ -2009,8 +2085,32 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, return queries } +// nextAvailableRefId returns the next unused refId using the same sequence as the +// frontend helper (A, B, ..., Z, AA, AB, ...). +func nextAvailableRefId(existing map[string]bool) string { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + var refIdFromIndex func(num int) string + refIdFromIndex = func(num int) string { + if num < len(letters) { + return string(letters[num]) + } + return refIdFromIndex(num/len(letters)-1) + string(letters[num%len(letters)]) + } + + for i := 0; ; i++ { + refId := refIdFromIndex(i) + if !existing[refId] { + return refId + } + } +} + func transformSingleQuery(ctx context.Context, targetMap map[string]interface{}, panelDatasource *dashv2alpha1.DashboardDataSourceRef, dsIndexProvider schemaversion.DataSourceIndexProvider) dashv2alpha1.DashboardPanelQueryKind { refId := schemaversion.GetStringValue(targetMap, "refId", "A") + if refId == "" { + refId = "A" + } hidden := getBoolField(targetMap, "hide", false) // Extract datasource from query or use panel datasource @@ -2170,7 +2270,7 @@ func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.Dashboar if linkMap, ok := link.(map[string]interface{}); ok { dataLink := dashv2alpha1.DashboardDataLink{ Title: schemaversion.GetStringValue(linkMap, "title"), - Url: schemaversion.GetStringValue(linkMap, "url"), + Url: stripBOM(schemaversion.GetStringValue(linkMap, "url")), } if _, exists := linkMap["targetBlank"]; exists { targetBlank := getBoolField(linkMap, "targetBlank", false) @@ -2184,6 +2284,32 @@ func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.Dashboar return result } +// knownPanelProperties lists all properties defined in the Panel schema (dashboard_kind.cue) +// that should NOT be passed to Angular migration handlers. Only "unknown" Angular-specific +// properties should be passed to migration handlers. +var knownPanelProperties = map[string]bool{ + "type": true, "id": true, "pluginVersion": true, "targets": true, + "title": true, "description": true, "transparent": true, "datasource": true, + "gridPos": true, "links": true, "repeat": true, "repeatDirection": true, + "maxPerRow": true, "maxDataPoints": true, "transformations": true, + "interval": true, "timeFrom": true, "timeShift": true, "hideTimeOverride": true, + "timeCompare": true, "libraryPanel": true, "cacheTimeout": true, + "queryCachingTTL": true, "options": true, "fieldConfig": true, "autoMigrateFrom": true, +} + +// extractAngularOptions extracts only the Angular-specific options from a panel map, +// filtering out all known Panel schema properties. This is used to pass just the +// Angular options to migration handlers (e.g., sparkline, valueName, format for singlestat). +func extractAngularOptions(panelMap map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for key, value := range panelMap { + if !knownPanelProperties[key] { + result[key] = value + } + } + return result +} + func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizConfigKind { panelType := schemaversion.GetStringValue(panelMap, "type", "timeseries") pluginVersion := schemaversion.GetStringValue(panelMap, "pluginVersion") @@ -2196,7 +2322,10 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo options := make(map[string]interface{}) if opts, ok := panelMap["options"].(map[string]interface{}); ok { - options = opts + // Deep copy options to avoid modifying the original + for k, v := range opts { + options[k] = v + } } // Add frontend-style default options to match frontend behavior @@ -2207,11 +2336,43 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo options["legend"] = legend } + // Handle Angular panel migrations + // This replicates the v0→v1 migration logic for panels that weren't migrated yet. + // We check two cases: + // 1. Panel already has autoMigrateFrom set (from v0→v1 migration) - panel type already converted + // 2. Panel type is a known Angular panel - need to convert type AND set autoMigrateFrom + // 3. Panel has original options - need to set autoMigrateFrom and originalOptions + autoMigrateFrom, hasAutoMigrateFrom := panelMap["autoMigrateFrom"].(string) + originalOptions := extractAngularOptions(panelMap) + + if !hasAutoMigrateFrom || autoMigrateFrom == "" { + // Check if panel type is an Angular type that needs migration + if newType := getAngularPanelMigration(panelType, panelMap); newType != "" { + autoMigrateFrom = panelType // Original Angular type + panelType = newType // New modern type + } else if len(originalOptions) > 0 { + autoMigrateFrom = panelType + } + } + + if autoMigrateFrom != "" { + options["__angularMigration"] = map[string]interface{}{ + "autoMigrateFrom": autoMigrateFrom, + "originalOptions": originalOptions, + } + } + + // Strip BOMs from options (may contain dataLinks with URLs that have BOMs) + cleanedOptions := stripBOMFromInterface(options) + if cleanedMap, ok := cleanedOptions.(map[string]interface{}); ok { + options = cleanedMap + } + // 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 + Kind: panelType, // Use panelType as Kind (plugin ID) - may be converted from Angular type Spec: dashv2alpha1.DashboardVizConfigSpec{ PluginVersion: pluginVersion, FieldConfig: fieldConfigSource, @@ -2350,9 +2511,14 @@ func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.Da hasDefaults = true } - // Extract array field + // Extract array field - strip BOMs from link URLs if linksArray, ok := extractArrayField(defaults, "links"); ok { - fieldConfigDefaults.Links = linksArray + cleanedLinks := stripBOMFromInterface(linksArray) + if cleanedArray, ok := cleanedLinks.([]interface{}); ok { + fieldConfigDefaults.Links = cleanedArray + } else { + fieldConfigDefaults.Links = linksArray + } hasDefaults = true } @@ -2507,22 +2673,15 @@ func buildRegexMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardReg 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{}) + optMap, ok := mappingMap["options"].(map[string]interface{}) if !ok { return nil } r := dashv2alpha1.DashboardV2alpha1RegexMapOptions{} - if pattern, ok := optMap["regex"].(string); ok { + if pattern, ok := optMap["pattern"].(string); ok { r.Pattern = pattern } - - // Result is a DashboardValueMappingResult if resMap, ok := optMap["result"].(map[string]interface{}); ok { r.Result = buildValueMappingResult(resMap) } @@ -2645,9 +2804,11 @@ func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alp fieldOverride.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, 0, len(propertiesArray)) for _, property := range propertiesArray { if propertyMap, ok := property.(map[string]interface{}); ok { + // Strip BOMs from property values (may contain links with URLs) + cleanedValue := stripBOMFromInterface(propertyMap["value"]) fieldOverride.Properties = append(fieldOverride.Properties, dashv2alpha1.DashboardDynamicConfigValue{ Id: schemaversion.GetStringValue(propertyMap, "id"), - Value: propertyMap["value"], + Value: cleanedValue, }) } } @@ -2659,3 +2820,10 @@ func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alp return result } + +// getAngularPanelMigration is a convenience wrapper around schemaversion.GetAngularPanelMigration. +// It checks if a panel type is an Angular panel and returns the new type to migrate to. +// Returns the new panel type if migration is needed, empty string otherwise. +func getAngularPanelMigration(panelType string, panelMap map[string]interface{}) string { + return schemaversion.GetAngularPanelMigration(panelType, panelMap) +} diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go index 3dad9188fe7..6c0e8f4f919 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go @@ -19,7 +19,7 @@ func TestV1beta1ToV2alpha1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -210,6 +210,48 @@ func TestV1beta1ToV2alpha1(t *testing.T) { assert.Equal(t, "", query.Spec.Query.Kind, "Query kind should be empty when datasource is empty object {}") }, }, + { + name: "missing refIds are assigned while existing refIds are preserved", + createV1beta1: func() *dashv1.Dashboard { + return &dashv1.Dashboard{ + Spec: dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "title": "Test Dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "bargauge", + "targets": []interface{}{ + map[string]interface{}{ + "refId": "", + "scenarioId": "random_walk", + }, + map[string]interface{}{ + "refId": "A", + "scenarioId": "random_walk", + }, + map[string]interface{}{ + "refId": "", + "scenarioId": "random_walk", + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) + panel := v2alpha1.Spec.Elements["panel-1"].PanelKind + require.NotNil(t, panel) + + require.Len(t, panel.Spec.Data.Spec.Queries, 3) + assert.Equal(t, "B", panel.Spec.Data.Spec.Queries[0].Spec.RefId) + assert.Equal(t, "A", panel.Spec.Data.Spec.Queries[1].Spec.RefId) + assert.Equal(t, "C", panel.Spec.Data.Spec.Queries[2].Spec.RefId) + }, + }, } for _, tt := range testCases { diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index fee798d3ec5..beed5679b85 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -11,10 +11,10 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { // Convert v2alpha1 → v1beta1 first, then v1beta1 → v0 v1beta1 := &dashv1.Dashboard{} - if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope); err != nil { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv0.APIVERSION out.Kind = in.Kind @@ -26,8 +26,7 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s Source: in, }, } - // For errors, set status but don't return error - return nil + return err } // Convert v1beta1 → v0 @@ -43,8 +42,7 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s Source: in, }, } - // For errors, set status but don't return error - return nil + return err } // Update the stored version to reflect the original source @@ -53,13 +51,13 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s return nil } -func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv1.APIVERSION out.Kind = in.Kind // Convert the spec - if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope); err != nil { out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv2alpha1.VERSION), @@ -68,9 +66,7 @@ func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboa Source: in, }, } - - // For errors, set status but don't return error - return nil + return err } // Set successful conversion status @@ -129,8 +125,7 @@ func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, sco Source: in, }, } - // For errors, set status but don't return error - return nil + return err } // Convert v1beta1 → v0 @@ -146,7 +141,7 @@ func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, sco Source: in, }, } - return nil + return err } // Update the stored version to reflect the original source @@ -172,14 +167,13 @@ func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard Source: in, }, } - // For errors, set status but don't return error - return nil + return err } // Convert v2alpha1 → v1beta1 // Note: ConvertDashboard_V2alpha1_to_V1beta1 will set out.ObjectMeta from v2alpha1, // but we've already set it from the original input, so it will be preserved - if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope); err != nil { out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv2beta1.VERSION), @@ -188,8 +182,7 @@ func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard Source: in, }, } - // For errors, set status but don't return error - return nil + return err } // Set successful conversion status diff --git a/apps/dashboard/pkg/migration/conversion/v2_test.go b/apps/dashboard/pkg/migration/conversion/v2_test.go index 18e0713fa84..3e218da9f81 100644 --- a/apps/dashboard/pkg/migration/conversion/v2_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2_test.go @@ -18,7 +18,7 @@ func TestV2alpha1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V2alpha1_to_V1beta1 sets status on conversion", func(t *testing.T) { // Create a dashboard for conversion @@ -39,7 +39,7 @@ func TestV2alpha1ConversionErrorHandling(t *testing.T) { } target := &dashv1.Dashboard{} - err := Convert_V2alpha1_to_V1beta1(source, target, nil, dsProvider) + err := Convert_V2alpha1_to_V1beta1(source, target, nil) // Convert_V2alpha1_to_V1beta1 doesn't return error, just sets status require.NoError(t, err, "Convert_V2alpha1_to_V1beta1 doesn't return error") @@ -90,7 +90,7 @@ func TestV2beta1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V2beta1_to_V1beta1 sets status on first step failure", func(t *testing.T) { // Create a dashboard that might cause conversion to fail on first step (v2beta1 -> v2alpha1) @@ -186,3 +186,141 @@ func TestV2beta1ConversionErrorHandling(t *testing.T) { } }) } + +// TestV2ConversionConsistency_SuccessStatusMustBeSet ensures v2 conversion functions +// set Status.Conversion with Failed=false on successful conversion. +func TestV2ConversionConsistency_SuccessStatusMustBeSet(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Create valid v2alpha1 dashboard + validV2alpha1 := &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{}, + }, + }, + }, + } + + // Create valid v2beta1 dashboard + validV2beta1 := &dashv2beta1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-dashboard", + }, + Spec: dashv2beta1.DashboardSpec{ + Title: "test dashboard", + Layout: dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + }, + }, + } + + t.Run("Convert_V2alpha1_to_V1beta1 must set success status", func(t *testing.T) { + target := &dashv1.Dashboard{} + err := Convert_V2alpha1_to_V1beta1(validV2alpha1, target, nil) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + }) + + t.Run("Convert_V2alpha1_to_V2beta1 must set success status", func(t *testing.T) { + target := &dashv2beta1.Dashboard{} + err := Convert_V2alpha1_to_V2beta1(validV2alpha1, target, nil) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + }) + + t.Run("Convert_V2beta1_to_V1beta1 must set success status", func(t *testing.T) { + target := &dashv1.Dashboard{} + err := Convert_V2beta1_to_V1beta1(validV2beta1, target, nil, dsProvider) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + }) + + t.Run("Convert_V2beta1_to_V2alpha1 must set success status", func(t *testing.T) { + target := &dashv2alpha1.Dashboard{} + err := Convert_V2beta1_to_V2alpha1(validV2beta1, target, nil) + require.NoError(t, err) + require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success") + require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success") + require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set") + }) +} + +// TestV2ConversionConsistency_ErrorsMustBeReturned ensures v2 conversion functions +// return errors instead of swallowing them. This prevents metrics and logs from being silently dropped. +func TestV2ConversionConsistency_ErrorsMustBeReturned(t *testing.T) { + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) + + // Note: v2 conversions are harder to make fail since they don't go through schema migration. + // These tests verify that IF an error occurs, it is returned (not swallowed). + // The existing tests already cover this, but we add explicit assertions here. + + t.Run("Convert_V2alpha1_to_V2beta1 returns error on conversion failure", func(t *testing.T) { + // Valid dashboard should succeed + 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) + // This should succeed, verifying the function works + require.NoError(t, err) + require.False(t, target.Status.Conversion.Failed) + }) + + t.Run("Convert_V2beta1_to_V2alpha1 returns error on conversion failure", func(t *testing.T) { + // Valid dashboard should succeed + source := &dashv2beta1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-dashboard", + }, + Spec: dashv2beta1.DashboardSpec{ + Title: "test dashboard", + Layout: dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{}, + }, + }, + }, + } + target := &dashv2alpha1.Dashboard{} + err := Convert_V2beta1_to_V2alpha1(source, target, nil) + // This should succeed, verifying the function works + require.NoError(t, err) + require.False(t, target.Status.Conversion.Failed) + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 180a8d603bb..e2a3e17843c 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -1,14 +1,12 @@ package conversion import ( - "context" "fmt" - "k8s.io/apimachinery/pkg/conversion" - dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "k8s.io/apimachinery/pkg/conversion" ) // ConvertDashboard_V2alpha1_to_V1beta1 converts a v2alpha1 dashboard to v1beta1 format. @@ -16,19 +14,13 @@ import ( // that represents the v1 dashboard JSON format. // The dsIndexProvider is used to resolve default datasources when queries/variables/annotations // don't have explicit datasource references. -func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv1.APIVERSION out.Kind = in.Kind // Preserve the Kind from input (should be "Dashboard") - // Get datasource index for resolving default datasources - var dsIndex *schemaversion.DatasourceIndex - if dsIndexProvider != nil { - dsIndex = dsIndexProvider.Index(context.Background()) - } - // Convert the spec to v1beta1 unstructured format - dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec, dsIndex) + dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec) if err != nil { return fmt.Errorf("failed to convert dashboard spec: %w", err) } @@ -39,7 +31,7 @@ func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv return nil } -func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (map[string]interface{}, error) { dashboard := make(map[string]interface{}) // Convert basic fields @@ -75,7 +67,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert panels from elements and layout - panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout, dsIndex) + panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout) if err != nil { return nil, fmt.Errorf("failed to convert panels: %w", err) } @@ -90,7 +82,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert variables - variables := convertVariablesToV1(in.Variables, dsIndex) + variables := convertVariablesToV1(in.Variables) if len(variables) > 0 { dashboard["templating"] = map[string]interface{}{ "list": variables, @@ -98,7 +90,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert annotations - always include even if empty to prevent DashboardModel from adding built-in - annotations := convertAnnotationsToV1(in.Annotations, dsIndex) + annotations := convertAnnotationsToV1(in.Annotations) dashboard["annotations"] = map[string]interface{}{ "list": annotations, } @@ -236,28 +228,28 @@ func countTotalPanels(panels []interface{}) int { // - RowsLayout: Rows become row panels; nested structures are flattened // - AutoGridLayout: Calculates gridPos based on column count and row height // - TabsLayout: Tabs become expanded row panels; content is flattened -func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) ([]interface{}, error) { if layout.GridLayoutKind != nil { - return convertGridLayoutToPanels(elements, layout.GridLayoutKind, dsIndex) + return convertGridLayoutToPanels(elements, layout.GridLayoutKind) } if layout.RowsLayoutKind != nil { - return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind, dsIndex) + return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind) } if layout.AutoGridLayoutKind != nil { - return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind, dsIndex) + return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind) } if layout.TabsLayoutKind != nil { - return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind, dsIndex) + return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind) } // No layout specified, return empty panels return []interface{}{}, nil } -func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind) ([]interface{}, error) { panels := make([]interface{}, 0, len(gridLayout.Spec.Items)) for _, item := range gridLayout.Spec.Items { @@ -266,7 +258,7 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name) } - panel, err := convertPanelFromElement(&element, &item, dsIndex) + panel, err := convertPanelFromElement(&element, &item) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -279,21 +271,21 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement // convertRowsLayoutToPanels converts a RowsLayout to V1 panels. // All nested structures (rows within rows, tabs within rows) are flattened to the root level. // Each row becomes a row panel, and nested content is added sequentially after it. -func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { - return convertNestedLayoutToPanels(elements, rowsLayout, nil, dsIndex, 0) +func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind) ([]interface{}, error) { + return convertNestedLayoutToPanels(elements, rowsLayout, nil, 0) } // convertNestedLayoutToPanels handles arbitrary nesting of RowsLayout and TabsLayout. // It processes each row/tab in order, tracking Y position to ensure panels don't overlap. // The function recursively flattens nested structures to produce a flat V1 panel array. -func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) { +func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, yOffset int64) ([]interface{}, error) { panels := make([]interface{}, 0) currentY := yOffset // Process RowsLayout if rowsLayout != nil { for _, row := range rowsLayout.Spec.Rows { - rowPanels, newY, err := processRowItem(elements, &row, dsIndex, currentY) + rowPanels, newY, err := processRowItem(elements, &row, currentY) if err != nil { return nil, err } @@ -305,7 +297,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme // Process TabsLayout (tabs are converted to rows) if tabsLayout != nil { for _, tab := range tabsLayout.Spec.Tabs { - tabPanels, newY, err := processTabItem(elements, &tab, dsIndex, currentY) + tabPanels, newY, err := processTabItem(elements, &tab, currentY) if err != nil { return nil, err } @@ -324,7 +316,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme // - Collapsed row: Panels stored inside row.panels with absolute Y positions // - Expanded row: Panels added to top level after the row panel // - Nested layouts: Parent row is preserved; nested content is flattened after it -func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) { +func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) currentY := startY @@ -354,7 +346,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash } // Then process nested rows - nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, currentY) if err != nil { return nil, 0, err } @@ -387,7 +379,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash } // Then process nested tabs - nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -429,7 +421,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Add collapsed panels if row is collapsed (panels use absolute Y positions) if isCollapsed { - collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY+1) + collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY+1) if err != nil { return nil, 0, err } @@ -444,7 +436,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Add panels from row layout (only for expanded rows or hidden header rows) if !isCollapsed || isHiddenHeader { - rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, dsIndex, currentY, isHiddenHeader, startY) + rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, currentY, isHiddenHeader, startY) if err != nil { return nil, 0, err } @@ -459,7 +451,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Each tab becomes an expanded row panel (collapsed=false) with an empty panels array. // The tab's content is flattened and added to the top level after the row panel. // Nested layouts within the tab are recursively processed. -func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) { +func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) currentY := startY @@ -487,7 +479,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash // Handle nested layouts inside the tab if tab.Spec.Layout.RowsLayoutKind != nil { // Nested RowsLayout inside tab - nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, currentY) if err != nil { return nil, 0, err } @@ -495,7 +487,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash currentY = getMaxYFromPanels(nestedPanels, currentY) } else if tab.Spec.Layout.TabsLayoutKind != nil { // Nested TabsLayout inside tab - nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -503,6 +495,9 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash currentY = getMaxYFromPanels(nestedPanels, currentY) } else if tab.Spec.Layout.GridLayoutKind != nil { // GridLayout inside tab + baseY := currentY + maxY := currentY + for _, item := range tab.Spec.Layout.GridLayoutKind.Spec.Items { element, ok := elements[item.Spec.Element.Name] if !ok { @@ -510,22 +505,24 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash } adjustedItem := item - adjustedItem.Spec.Y = item.Spec.Y + currentY + adjustedItem.Spec.Y = item.Spec.Y + baseY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } panels = append(panels, panel) panelEndY := adjustedItem.Spec.Y + item.Spec.Height - if panelEndY > currentY { - currentY = panelEndY + if panelEndY > maxY { + maxY = panelEndY } } + + currentY = maxY } else if tab.Spec.Layout.AutoGridLayoutKind != nil { // AutoGridLayout inside tab - convert with Y offset - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, dsIndex, currentY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -540,7 +537,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash // Panels are positioned with absolute Y coordinates (baseY + relative Y). // This matches V1 behavior where collapsed row panels store their children // with Y positions as if the row were expanded at that location. -func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) { +func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, baseY int64) ([]interface{}, error) { panels := make([]interface{}, 0) if layout.GridLayoutKind != nil { @@ -552,7 +549,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo // Create a copy with adjusted Y position adjustedItem := item adjustedItem.Spec.Y = item.Spec.Y + baseY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -561,7 +558,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo } // Handle AutoGridLayout for collapsed rows with Y offset if layout.AutoGridLayoutKind != nil { - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY) if err != nil { return nil, err } @@ -571,7 +568,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo if layout.RowsLayoutKind != nil { currentY := baseY for _, row := range layout.RowsLayoutKind.Spec.Rows { - nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY) if err != nil { return nil, err } @@ -582,7 +579,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo if layout.TabsLayoutKind != nil { currentY := baseY for _, tab := range layout.TabsLayoutKind.Spec.Tabs { - nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY) if err != nil { return nil, err } @@ -596,7 +593,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo // extractCollapsedPanelsFromTabLayoutWithAbsoluteY extracts panels from a tab layout with absolute Y. // Similar to extractCollapsedPanelsWithAbsoluteY but handles the tab-specific layout type. -func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) { +func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, baseY int64) ([]interface{}, error) { panels := make([]interface{}, 0) if layout.GridLayoutKind != nil { @@ -607,7 +604,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 } adjustedItem := item adjustedItem.Spec.Y = item.Spec.Y + baseY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -615,7 +612,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 } } if layout.AutoGridLayoutKind != nil { - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY) if err != nil { return nil, err } @@ -624,7 +621,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 if layout.RowsLayoutKind != nil { currentY := baseY for _, row := range layout.RowsLayoutKind.Spec.Rows { - nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY) if err != nil { return nil, err } @@ -635,7 +632,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 if layout.TabsLayoutKind != nil { currentY := baseY for _, tab := range layout.TabsLayoutKind.Spec.Tabs { - nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY) if err != nil { return nil, err } @@ -679,7 +676,7 @@ func getLayoutHeightFromTab(layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLa // - Explicit row: Add (currentY - 1) to relative Y for absolute positioning // // Returns the panels and the new Y position for the next row. -func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) { +func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) // For hidden headers, don't track Y changes (matches original behavior) maxY := startY @@ -700,7 +697,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la } // For hidden headers: don't adjust Y, keep item.Spec.Y as-is - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -725,7 +722,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la yOffset = currentY - 1 } - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, yOffset) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, yOffset) if err != nil { return nil, 0, err } @@ -788,7 +785,7 @@ func getLayoutHeight(layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayou // convertAutoGridLayoutToPanelsWithOffset converts AutoGridLayout with a Y offset. // Same as convertAutoGridLayoutToPanels but starts at yOffset instead of 0. // Used when AutoGridLayout appears inside rows or tabs. -func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) { +func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, yOffset int64) ([]interface{}, error) { panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items)) const ( @@ -850,7 +847,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da }, } - panel, err := convertPanelFromElement(&element, &gridItem, dsIndex) + panel, err := convertPanelFromElement(&element, &gridItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -876,7 +873,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da // // Width: 24 / maxColumnCount (default 3 columns = 8 units wide) // Height: Predefined grid units per mode (see pixelsToGridUnits for custom) -func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind) ([]interface{}, error) { panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items)) const ( @@ -963,7 +960,7 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle } } - panel, err := convertPanelFromElement(&element, &gridItem, dsIndex) + panel, err := convertPanelFromElement(&element, &gridItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -984,11 +981,11 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle // V1 has no native tab concept, so tabs are converted to expanded row panels. // Each tab becomes a row panel (collapsed=false, panels=[]) with its content // flattened to the top level. Tab order is preserved in the output. -func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { - return convertNestedLayoutToPanels(elements, nil, tabsLayout, dsIndex, 0) +func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind) ([]interface{}, error) { + return convertNestedLayoutToPanels(elements, nil, tabsLayout, 0) } -func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind) (map[string]interface{}, error) { panel := make(map[string]interface{}) // Set grid position @@ -1017,7 +1014,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem } if element.PanelKind != nil { - return convertPanelKindToV1(element.PanelKind, panel, dsIndex) + return convertPanelKindToV1(element.PanelKind, panel) } if element.LibraryPanelKind != nil { @@ -1027,7 +1024,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem return nil, fmt.Errorf("element has neither PanelKind nor LibraryPanelKind") } -func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}) (map[string]interface{}, error) { spec := panelKind.Spec panel["id"] = int(spec.Id) @@ -1067,16 +1064,17 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[ } // Convert queries (targets) - targets := make([]map[string]interface{}, 0, len(spec.Data.Spec.Queries)) + // Use []interface{} for consistency with JSON unmarshaling and other code paths + targets := make([]interface{}, 0, len(spec.Data.Spec.Queries)) for _, query := range spec.Data.Spec.Queries { - target := convertPanelQueryToV1(&query, dsIndex) + target := convertPanelQueryToV1(&query) targets = append(targets, target) } panel["targets"] = targets // Detect mixed datasource - set panel.datasource to "mixed" if queries use different datasources // This matches the frontend behavior in getPanelDataSource (layoutSerializers/utils.ts) - if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries, dsIndex); mixedDS != nil { + if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries); mixedDS != nil { panel["datasource"] = mixedDS } @@ -1125,7 +1123,7 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[ return panel, nil } -func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex *schemaversion.DatasourceIndex) map[string]interface{} { +func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} { target := make(map[string]interface{}) // Copy query spec (excluding refId, hide, datasource which are handled separately) @@ -1150,7 +1148,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex } // Resolve datasource based on V2 input (reuse shared function) - datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind, nil) + datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind) if datasource != nil { target["datasource"] = datasource } @@ -1164,7 +1162,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex // - Else if queryKind (type) is non-empty → return {type} only // - Else → return nil (no datasource) // Used for variables and annotations. Panel queries use convertPanelQueryToV1Target. -func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string, _ *schemaversion.DatasourceIndex) map[string]interface{} { +func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string) map[string]interface{} { // Case 1: Explicit datasource with UID provided if explicitDS != nil && explicitDS.Uid != nil && *explicitDS.Uid != "" { datasource := map[string]interface{}{ @@ -1195,7 +1193,7 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer // Compares based on V2 input without runtime resolution: // - If query has explicit datasource.uid → use that UID and type // - Else → use query.Kind as type (empty UID) -func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind, _ *schemaversion.DatasourceIndex) map[string]interface{} { +func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} { if len(queries) == 0 { return nil } @@ -1254,7 +1252,7 @@ func convertLibraryPanelKindToV1(libPanelKind *dashv2alpha1.DashboardLibraryPane return panel, nil } -func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} { +func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(variables)) for _, variable := range variables { @@ -1262,7 +1260,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde var err error if variable.QueryVariableKind != nil { - varMap, err = convertQueryVariableToV1(variable.QueryVariableKind, dsIndex) + varMap, err = convertQueryVariableToV1(variable.QueryVariableKind) } else if variable.DatasourceVariableKind != nil { varMap, err = convertDatasourceVariableToV1(variable.DatasourceVariableKind) } else if variable.CustomVariableKind != nil { @@ -1274,9 +1272,9 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde } else if variable.TextVariableKind != nil { varMap, err = convertTextVariableToV1(variable.TextVariableKind) } else if variable.GroupByVariableKind != nil { - varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind, dsIndex) + varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind) } else if variable.AdhocVariableKind != nil { - varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind, dsIndex) + varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind) } else if variable.SwitchVariableKind != nil { varMap, err = convertSwitchVariableToV1(variable.SwitchVariableKind) } @@ -1289,7 +1287,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde return result } -func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1320,6 +1318,9 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, if spec.Definition != nil { varMap["definition"] = *spec.Definition } + if spec.RegexApplyTo != nil { + varMap["regexApplyTo"] = string(*spec.RegexApplyTo) + } varMap["allowCustomValue"] = spec.AllowCustomValue // Convert query - handle LEGACY_STRING_VALUE_KEY @@ -1336,7 +1337,7 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, } // Resolve datasource - use explicit datasource or resolve from query kind (datasource type)/default - datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind, dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind) if datasource != nil { varMap["datasource"] = datasource } @@ -1486,7 +1487,7 @@ func convertTextVariableToV1(variable *dashv2alpha1.DashboardTextVariableKind) ( return varMap, nil } -func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1509,7 +1510,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK } // Resolve datasource - GroupBy variables don't have a query kind, so use empty string (will fall back to default) - datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, "") if datasource != nil { varMap["datasource"] = datasource } @@ -1517,7 +1518,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK return varMap, nil } -func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1536,7 +1537,7 @@ func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind, varMap["allowCustomValue"] = spec.AllowCustomValue // Resolve datasource - Adhoc variables don't have a query kind, so use empty string (will fall back to default) - datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, "") if datasource != nil { varMap["datasource"] = datasource } @@ -1663,7 +1664,7 @@ func convertSwitchVariableToV1(variable *dashv2alpha1.DashboardSwitchVariableKin return varMap, nil } -func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} { +func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(annotations)) for _, annotation := range annotations { @@ -1686,7 +1687,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK if annotation.Spec.Query != nil { queryKind = annotation.Spec.Query.Kind } - datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind, dsIndex) + datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind) if datasource != nil { annotationMap["datasource"] = datasource } @@ -1972,16 +1973,16 @@ func convertFieldConfigOverridesToV1(overrides []dashv2alpha1.DashboardV2alpha1F "options": override.Matcher.Options, } + properties := make([]map[string]interface{}, 0, len(override.Properties)) if len(override.Properties) > 0 { - properties := make([]map[string]interface{}, 0, len(override.Properties)) for _, prop := range override.Properties { properties = append(properties, map[string]interface{}{ "id": prop.Id, "value": prop.Value, }) } - overrideMap["properties"] = properties } + overrideMap["properties"] = properties result = append(result, overrideMap) } @@ -2073,11 +2074,9 @@ func convertRegexMapToV1(regexMap *dashv2alpha1.DashboardRegexMap) map[string]in return nil } - options := []map[string]interface{}{ - { - "pattern": regexMap.Options.Pattern, - "result": convertValueMappingResultToV1(regexMap.Options.Result), - }, + options := map[string]interface{}{ + "pattern": regexMap.Options.Pattern, + "result": convertValueMappingResultToV1(regexMap.Options.Result), } return map[string]interface{}{ diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go index 6ed9804fc68..b3b4fb98395 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go @@ -282,7 +282,7 @@ func TestV2alpha1ToV1beta1LayoutErrors(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -498,7 +498,7 @@ func TestV2alpha1ToV1beta1BasicFields(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 452f0140047..5b3a0d115b8 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -767,6 +767,7 @@ func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQuer out.SkipUrlSync = in.SkipUrlSync out.Description = in.Description out.Regex = in.Regex + out.RegexApplyTo = (*dashv2beta1.DashboardVariableRegexApplyTo)(in.RegexApplyTo) out.Sort = dashv2beta1.DashboardVariableSort(in.Sort) out.Definition = in.Definition out.Options = convertVariableOptions_V2alpha1_to_V2beta1(in.Options) diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go index 028197bd655..e92c9215c34 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -18,7 +18,7 @@ func TestV2alpha1ToV2beta1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 95dfcf76c9d..aa7e8e5f36d 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -806,6 +806,7 @@ func convertQueryVariableSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQuery out.SkipUrlSync = in.SkipUrlSync out.Description = in.Description out.Regex = in.Regex + out.RegexApplyTo = (*dashv2alpha1.DashboardVariableRegexApplyTo)(in.RegexApplyTo) out.Sort = dashv2alpha1.DashboardVariableSort(in.Sort) out.Definition = in.Definition out.Options = convertVariableOptions_V2beta1_to_V2alpha1(in.Options) diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 23dcd36ee17..8b84ab40928 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -24,7 +24,7 @@ func TestV2beta1ToV2alpha1RoundTrip(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -107,7 +107,7 @@ func TestV2beta1ToV2alpha1FromOutputFiles(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -193,7 +193,7 @@ func TestV2beta1ToV2alpha1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/frontend_defaults.go b/apps/dashboard/pkg/migration/frontend_defaults.go index 1df28c4de27..e8c779406a6 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults.go +++ b/apps/dashboard/pkg/migration/frontend_defaults.go @@ -940,31 +940,12 @@ func applyPanelAutoMigration(panel map[string]interface{}) { return } - var newType string - // Graph needs special logic as it can be migrated to multiple panels // Including graphite which was previously migrated to graph in the schema version 2 migration in DashboardMigrator.ts // but this was a bug because in there graphite was set to graph, but since those migrations run // after PanelModel.restoreModel where autoMigrateFrom is set, this caused the graph migration to be skipped. // And this resulted in a dashboard with invalid panels. - if panelType == "graph" || panelType == "graphite" { - // Check xaxis mode for special cases - newType = getGraphAutoMigration(panel) - } else { - // Check autoMigrateAngular mapping - autoMigrateAngular := map[string]string{ - "table-old": "table", - "singlestat": "stat", - "grafana-singlestat-panel": "stat", - "grafana-piechart-panel": "piechart", - "grafana-worldmap-panel": "geomap", - "natel-discrete-panel": "state-timeline", - } - - if mappedType, exists := autoMigrateAngular[panelType]; exists { - newType = mappedType - } - } + newType := schemaversion.GetAngularPanelMigration(panelType, panel) // Apply auto-migration if a new type was determined if newType != "" { @@ -973,36 +954,6 @@ func applyPanelAutoMigration(panel map[string]interface{}) { } } -func getGraphAutoMigration(panel map[string]interface{}) string { - newType := "" - if xaxis, ok := panel["xaxis"].(map[string]interface{}); ok { - if mode, ok := xaxis["mode"].(string); ok { - switch mode { - case "series": - // Check legend values for bargauge - if legend, ok := panel["legend"].(map[string]interface{}); ok { - if values, ok := legend["values"].(bool); ok && values { - newType = "bargauge" - } else { - newType = "barchart" - } - } else { - newType = "barchart" - } - case "histogram": - newType = "histogram" - } - } - } - - // Default graph migration to timeseries - if newType == "" { - newType = "timeseries" - } - - return newType -} - // removeNullValuesRecursively removes null values from nested objects and arrays // This matches the frontend's JSON.stringify/parse behavior func removeNullValuesRecursively(data interface{}) { diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 9c87a45b2db..d1b0fcbb7af 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -4,13 +4,19 @@ import ( "context" "fmt" "sync" + "time" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) +// DefaultCacheTTL is the default TTL for the datasource and library element caches. +const DefaultCacheTTL = time.Minute + // Initialize provides the migrator singleton with required dependencies and builds the map of migrations. -func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) { - migratorInstance.init(dsIndexProvider, leIndexProvider) +func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) { + migratorInstance.init(dsIndexProvider, leIndexProvider, cacheTTL) } // GetDataSourceIndexProvider returns the datasource index provider instance that was initialized. @@ -38,6 +44,34 @@ func ResetForTesting() { initOnce = sync.Once{} } +// PreloadCache preloads the datasource and library element caches for the given namespaces. +func PreloadCache(ctx context.Context, nsInfos []types.NamespaceInfo) { + // Wait for initialization to complete + <-migratorInstance.ready + + // Try to preload datasource cache + if preloadable, ok := migratorInstance.dsIndexProvider.(schemaversion.PreloadableCache); ok { + preloadable.Preload(ctx, nsInfos) + } + + // Try to preload library element cache + if preloadable, ok := migratorInstance.leIndexProvider.(schemaversion.PreloadableCache); ok { + preloadable.Preload(ctx, nsInfos) + } +} + +// PreloadCacheInBackground starts a goroutine that preloads the caches for the given namespaces. +func PreloadCacheInBackground(nsInfos []types.NamespaceInfo) { + go func() { + defer func() { + if r := recover(); r != nil { + logging.DefaultLogger.Error("panic during cache preloading", "error", r) + } + }() + PreloadCache(context.Background(), nsInfos) + }() +} + // Migrate migrates the given dashboard to the target version. // This will block until the migrator is initialized. func Migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error { @@ -59,11 +93,15 @@ type migrator struct { leIndexProvider schemaversion.LibraryElementIndexProvider } -func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) { +func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) { initOnce.Do(func() { - m.dsIndexProvider = dsIndexProvider - m.leIndexProvider = leIndexProvider - m.migrations = schemaversion.GetMigrations(dsIndexProvider, leIndexProvider) + // Wrap the provider with org-aware TTL caching for all conversions. + // This prevents repeated DB queries across multiple conversion calls while allowing + // the cache to refresh periodically, making it suitable for long-lived singleton usage. + m.dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider, cacheTTL) + // Wrap library element provider with caching as well + m.leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider, cacheTTL) + m.migrations = schemaversion.GetMigrations(m.dsIndexProvider, m.leIndexProvider) close(m.ready) }) } diff --git a/apps/dashboard/pkg/migration/migrate_test.go b/apps/dashboard/pkg/migration/migrate_test.go index 8bb150f91ca..4fe4ea90a60 100644 --- a/apps/dashboard/pkg/migration/migrate_test.go +++ b/apps/dashboard/pkg/migration/migrate_test.go @@ -10,10 +10,13 @@ import ( "path/filepath" "strconv" "strings" + "sync/atomic" "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" @@ -31,7 +34,7 @@ func TestMigrate(t *testing.T) { ResetForTesting() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) t.Run("minimum version check", func(t *testing.T) { err := Migrate(context.Background(), map[string]interface{}{ @@ -49,7 +52,7 @@ func TestMigrateSingleVersion(t *testing.T) { // Use the same datasource provider as the frontend test to ensure consistency dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) runSingleVersionMigrationTests(t, SINGLE_VERSION_OUTPUT_DIR) } @@ -218,7 +221,7 @@ func TestSchemaMigrationMetrics(t *testing.T) { // Initialize migration with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -304,7 +307,7 @@ func TestSchemaMigrationMetrics(t *testing.T) { func TestSchemaMigrationLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) tests := []struct { name string @@ -423,7 +426,7 @@ func TestMigrateDevDashboards(t *testing.T) { ResetForTesting() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.DevDashboardConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) runDevDashboardMigrationTests(t, schemaversion.LATEST_VERSION, DEV_DASHBOARDS_OUTPUT_DIR) } @@ -449,3 +452,232 @@ func runDevDashboardMigrationTests(t *testing.T, targetVersion int, outputDir st }) } } + +func TestMigrateWithCache(t *testing.T) { + // Reset the migration singleton before each test + ResetForTesting() + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds-uid-1", Type: "prometheus", Name: "Prometheus", Default: true, APIVersion: "v1"}, + {UID: "ds-uid-2", Type: "loki", Name: "Loki", Default: false, APIVersion: "v1"}, + {UID: "ds-uid-3", Type: "prometheus", Name: "Prometheus 2", Default: false, APIVersion: "v1"}, + } + + // Create a dashboard at schema version 32 for V33 and V36 migration with datasource references + dashboard1 := map[string]interface{}{ + "schemaVersion": 32, + "title": "Test Dashboard 1", + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + // String datasource that V33 will migrate to object reference + "datasource": "Prometheus", + "targets": []interface{}{ + map[string]interface{}{ + "refId": "A", + "datasource": "Loki", + }, + }, + }, + }, + } + + // Create a dashboard at schema version 35 for testing V36 migration with datasource references in annotations + dashboard2 := map[string]interface{}{ + "schemaVersion": 35, + "title": "Test Dashboard 2", + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "Test Annotation", + "datasource": "Prometheus 2", // String reference that V36 should convert + "enable": true, + }, + }, + }, + } + + t.Run("with datasources", func(t *testing.T) { + ResetForTesting() + dsProvider := newCountingProvider(datasources) + leProvider := newCountingLibraryProvider(nil) + // Initialize the migration system with our counting providers + Initialize(dsProvider, leProvider, DefaultCacheTTL) + // Verify initial call count is zero + assert.Equal(t, dsProvider.GetCallCount(), int64(0)) + // Create a context with namespace (required for caching) + ctx := request.WithNamespace(context.Background(), "default") + + // First migration - should invoke the provider once to build the cache + dash1 := deepCopyDashboard(dashboard1) + err := Migrate(ctx, dash1, schemaversion.LATEST_VERSION) + require.NoError(t, err) + assert.Equal(t, int64(1), dsProvider.GetCallCount()) + + // Verify datasource conversion from string to object reference + panels := dash1["panels"].([]interface{}) + panel := panels[0].(map[string]interface{}) + panelDS, ok := panel["datasource"].(map[string]interface{}) + require.True(t, ok, "panel datasource should be converted to object") + assert.Equal(t, "ds-uid-1", panelDS["uid"]) + assert.Equal(t, "prometheus", panelDS["type"]) + + // Verify target datasource conversion + targets := panel["targets"].([]interface{}) + target := targets[0].(map[string]interface{}) + targetDS, ok := target["datasource"].(map[string]interface{}) + require.True(t, ok, "target datasource should be converted to object") + assert.Equal(t, "ds-uid-2", targetDS["uid"]) + assert.Equal(t, "loki", targetDS["type"]) + + // Migration with V35 dashboard - should use the cached index from first migration + dash2 := deepCopyDashboard(dashboard2) + err = Migrate(ctx, dash2, schemaversion.LATEST_VERSION) + require.NoError(t, err, "second migration should succeed") + assert.Equal(t, int64(1), dsProvider.GetCallCount()) + + // Verify the annotation datasource was converted to object reference + annotations := dash2["annotations"].(map[string]interface{}) + list := annotations["list"].([]interface{}) + var testAnnotation map[string]interface{} + for _, a := range list { + ann := a.(map[string]interface{}) + if ann["name"] == "Test Annotation" { + testAnnotation = ann + break + } + } + require.NotNil(t, testAnnotation, "Test Annotation should exist") + annotationDS, ok := testAnnotation["datasource"].(map[string]interface{}) + require.True(t, ok, "annotation datasource should be converted to object") + assert.Equal(t, "ds-uid-3", annotationDS["uid"]) + assert.Equal(t, "prometheus", annotationDS["type"]) + }) + + // tests that cache isolates data per namespace + t.Run("with multiple orgs", func(t *testing.T) { + // Reset the migration singleton + ResetForTesting() + dsProvider := newCountingProvider(datasources) + leProvider := newCountingLibraryProvider(nil) + Initialize(dsProvider, leProvider, DefaultCacheTTL) + // Create contexts for different orgs with proper namespace format (org-ID) + ctx1 := request.WithNamespace(context.Background(), "default") // org 1 + ctx2 := request.WithNamespace(context.Background(), "stacks-2") // stack 2 + + // Migrate for org 1 + err := Migrate(ctx1, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg1 := dsProvider.GetCallCount() + + // Migrate for org 2 - should build separate cache + err = Migrate(ctx2, deepCopyDashboard(dashboard2), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg2 := dsProvider.GetCallCount() + + assert.Greater(t, callsAfterOrg2, callsAfterOrg1, + "org 2 migration should have called provider (separate cache)") + + // Migrate again for org 1 - should use cache + err = Migrate(ctx1, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg1Again := dsProvider.GetCallCount() + assert.Equal(t, callsAfterOrg2, callsAfterOrg1Again, + "second org 1 migration should use cache") + + // Migrate again for org 2 - should use cache + err = Migrate(ctx2, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg2Again := dsProvider.GetCallCount() + assert.Equal(t, callsAfterOrg2, callsAfterOrg2Again, + "second org 2 migration should use cache") + }) +} + +// countingProvider wraps a datasource provider and counts calls to Index() +type countingProvider struct { + datasources []schemaversion.DataSourceInfo + callCount atomic.Int64 +} + +func newCountingProvider(datasources []schemaversion.DataSourceInfo) *countingProvider { + return &countingProvider{ + datasources: datasources, + } +} + +func (p *countingProvider) Index(_ context.Context) *schemaversion.DatasourceIndex { + p.callCount.Add(1) + return schemaversion.NewDatasourceIndex(p.datasources) +} + +func (p *countingProvider) GetCallCount() int64 { + return p.callCount.Load() +} + +// countingLibraryProvider wraps a library element provider and counts calls +type countingLibraryProvider struct { + elements []schemaversion.LibraryElementInfo + callCount atomic.Int64 +} + +func newCountingLibraryProvider(elements []schemaversion.LibraryElementInfo) *countingLibraryProvider { + return &countingLibraryProvider{ + elements: elements, + } +} + +func (p *countingLibraryProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + p.callCount.Add(1) + return p.elements +} + +func (p *countingLibraryProvider) GetCallCount() int64 { + return p.callCount.Load() +} + +// deepCopyDashboard creates a deep copy of a dashboard map +func deepCopyDashboard(dash map[string]interface{}) map[string]interface{} { + cpy := make(map[string]interface{}) + for k, v := range dash { + switch val := v.(type) { + case []interface{}: + cpy[k] = deepCopySlice(val) + case map[string]interface{}: + cpy[k] = deepCopyMapForCache(val) + default: + cpy[k] = v + } + } + return cpy +} + +func deepCopySlice(s []interface{}) []interface{} { + cpy := make([]interface{}, len(s)) + for i, v := range s { + switch val := v.(type) { + case []interface{}: + cpy[i] = deepCopySlice(val) + case map[string]interface{}: + cpy[i] = deepCopyMapForCache(val) + default: + cpy[i] = v + } + } + return cpy +} + +func deepCopyMapForCache(m map[string]interface{}) map[string]interface{} { + cpy := make(map[string]interface{}) + for k, v := range m { + switch val := v.(type) { + case []interface{}: + cpy[k] = deepCopySlice(val) + case map[string]interface{}: + cpy[k] = deepCopyMapForCache(val) + default: + cpy[k] = v + } + } + return cpy +} diff --git a/apps/dashboard/pkg/migration/schemaversion/cache.go b/apps/dashboard/pkg/migration/schemaversion/cache.go new file mode 100644 index 00000000000..2548c2cc5ba --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/cache.go @@ -0,0 +1,100 @@ +package schemaversion + +import ( + "context" + "sync" + "time" + + "github.com/hashicorp/golang-lru/v2/expirable" + "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" +) + +const defaultCacheSize = 1000 + +// CacheProvider is a generic cache interface for schema version providers. +type CacheProvider[T any] interface { + // Get returns the cached value if it's still valid, otherwise calls fetch and caches the result. + Get(ctx context.Context) T +} + +// PreloadableCache is an interface for providers that support preloading the cache. +type PreloadableCache interface { + // Preload loads data into the cache for the given namespaces. + Preload(ctx context.Context, nsInfos []types.NamespaceInfo) +} + +// cachedProvider is a thread-safe TTL cache that wraps any fetch function. +type cachedProvider[T any] struct { + fetch func(context.Context) T + cache *expirable.LRU[string, T] // LRU cache: namespace to cache entry + inFlight sync.Map // map[string]*sync.Mutex - per-namespace fetch locks +} + +// newCachedProvider creates a new cachedProvider. +// The fetch function should be able to handle context with different namespaces. +// A non-positive size turns LRU mechanism off (cache of unlimited size). +// A non-positive cacheTTL disables TTL expiration. +func newCachedProvider[T any](fetch func(context.Context) T, size int, cacheTTL time.Duration) *cachedProvider[T] { + cacheProvider := &cachedProvider[T]{ + fetch: fetch, + } + cacheProvider.cache = expirable.NewLRU(size, func(key string, value T) { + cacheProvider.inFlight.Delete(key) + }, cacheTTL) + return cacheProvider +} + +// Get returns the cached value if it's still valid, otherwise calls fetch and caches the result. +func (p *cachedProvider[T]) Get(ctx context.Context) T { + // Get namespace info from ctx + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + // No namespace, fall back to direct fetch call without caching + logging.FromContext(ctx).Warn("Unable to get namespace info from context, skipping cache") + return p.fetch(ctx) + } + + // Fast path: check if cache is still valid + if entry, ok := p.cache.Get(namespace); ok { + return entry + } + + // Get or create a per-namespace lock for this fetch operation + // This ensures only one fetch happens per namespace at a time + lockInterface, _ := p.inFlight.LoadOrStore(namespace, &sync.Mutex{}) + nsMutex := lockInterface.(*sync.Mutex) + + // Lock this specific namespace - other namespaces can still proceed + nsMutex.Lock() + defer nsMutex.Unlock() + + // Double-check: another goroutine might have already fetched while we waited + if entry, ok := p.cache.Get(namespace); ok { + return entry + } + + // Fetch outside the main lock - only this namespace is blocked + logging.FromContext(ctx).Debug("cache miss or expired, fetching new value", "namespace", namespace) + value := p.fetch(ctx) + + // Update the cache for this namespace + p.cache.Add(namespace, value) + + return value +} + +// Preload loads data into the cache for the given namespaces. +func (p *cachedProvider[T]) Preload(ctx context.Context, nsInfos []types.NamespaceInfo) { + // Build the cache using a context with the namespace + logging.FromContext(ctx).Info("preloading cache", "nsInfos", len(nsInfos)) + startedAt := time.Now() + defer func() { + logging.FromContext(ctx).Info("finished preloading cache", "nsInfos", len(nsInfos), "elapsed", time.Since(startedAt)) + }() + for _, nsInfo := range nsInfos { + p.cache.Add(nsInfo.Value, p.fetch(request.WithNamespace(ctx, nsInfo.Value))) + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/cache_test.go b/apps/dashboard/pkg/migration/schemaversion/cache_test.go new file mode 100644 index 00000000000..081455143e4 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/cache_test.go @@ -0,0 +1,478 @@ +package schemaversion + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/endpoints/request" + + authlib "github.com/grafana/authlib/types" +) + +// testProvider tracks how many times get() is called +type testProvider struct { + testData any + callCount atomic.Int64 +} + +func newTestProvider(testData any) *testProvider { + return &testProvider{ + testData: testData, + } +} + +func (p *testProvider) get(_ context.Context) any { + p.callCount.Add(1) + return p.testData +} + +func (p *testProvider) getCallCount() int64 { + return p.callCount.Load() +} + +func TestCachedProvider_CacheHit(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + {UID: "ds2", Type: "loki", Name: "Loki"}, + } + + underlying := newTestProvider(datasources) + // Test newCachedProvider directly instead of the wrapper + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute) + + // Use "default" namespace (org 1) - this is the standard Grafana namespace format + ctx := request.WithNamespace(context.Background(), "default") + + // First call should hit the underlying provider + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first call should invoke underlying provider") + + // Second call should use cache + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(1), underlying.getCallCount(), "second call should use cache, not invoke underlying provider") + + // Both should return the same data + assert.Equal(t, idx1, idx2) +} + +func TestCachedProvider_NamespaceIsolation(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute) + + // Use "default" (org 1) and "org-2" (org 2) - standard Grafana namespace formats + ctx1 := request.WithNamespace(context.Background(), "default") + ctx2 := request.WithNamespace(context.Background(), "org-2") + + // First call for org 1 + idx1 := cached.Get(ctx1) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first org-1 call should invoke underlying provider") + + // Call for org 2 should also invoke underlying provider (different namespace) + idx2 := cached.Get(ctx2) + require.NotNil(t, idx2) + assert.Equal(t, int64(2), underlying.getCallCount(), "org-2 call should invoke underlying provider (separate cache)") + + // Second call for org 1 should use cache + idx3 := cached.Get(ctx1) + require.NotNil(t, idx3) + assert.Equal(t, int64(2), underlying.getCallCount(), "second org-1 call should use cache") + + // Second call for org 2 should use cache + idx4 := cached.Get(ctx2) + require.NotNil(t, idx4) + assert.Equal(t, int64(2), underlying.getCallCount(), "second org-2 call should use cache") +} + +func TestCachedProvider_NoNamespaceFallback(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute) + + // Context without namespace - should fall back to direct provider call + ctx := context.Background() + + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount()) + + // Second call without namespace should also invoke underlying (no caching for unknown namespace) + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(2), underlying.getCallCount(), "without namespace, each call should invoke underlying provider") +} + +func TestCachedProvider_ConcurrentAccess(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute) + + // Use "default" namespace (org 1) + ctx := request.WithNamespace(context.Background(), "default") + + var wg sync.WaitGroup + numGoroutines := 100 + + // Launch many goroutines that all try to access the cache simultaneously + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }() + } + + wg.Wait() + + // Due to double-check locking, only 1 goroutine should have actually built the cache + // In practice, there might be a few more due to timing, but it should be much less than numGoroutines + callCount := underlying.getCallCount() + assert.LessOrEqual(t, callCount, int64(5), "with proper locking, very few goroutines should invoke underlying provider; got %d", callCount) +} + +func TestCachedProvider_ConcurrentNamespaces(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute) + + var wg sync.WaitGroup + numOrgs := 10 + callsPerOrg := 20 + + // Launch goroutines for multiple namespaces + // Use valid namespace formats: "default" for org 1, "org-N" for N > 1 + namespaces := make([]string, numOrgs) + namespaces[0] = "default" + for i := 1; i < numOrgs; i++ { + namespaces[i] = fmt.Sprintf("org-%d", i+1) + } + + for _, ns := range namespaces { + ctx := request.WithNamespace(context.Background(), ns) + for i := 0; i < callsPerOrg; i++ { + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }(ctx) + } + } + + wg.Wait() + + // Each org should have at most a few calls (ideally 1, but timing can cause a few more) + callCount := underlying.getCallCount() + // With 10 orgs, we expect around 10 calls (one per org) + assert.LessOrEqual(t, callCount, int64(numOrgs), "expected roughly one call per org, got %d calls for %d orgs", callCount, numOrgs) +} + +// Test that cache returns correct data for each namespace +func TestCachedProvider_CorrectDataPerNamespace(t *testing.T) { + // Provider that returns different data based on namespace + underlying := &namespaceAwareProvider{ + datasourcesByNamespace: map[string][]DataSourceInfo{ + "default": {{UID: "org1-ds", Type: "prometheus", Name: "Org1 DS", Default: true}}, + "org-2": {{UID: "org2-ds", Type: "loki", Name: "Org2 DS", Default: true}}, + }, + } + cached := newCachedProvider(underlying.Index, defaultCacheSize, time.Minute) + + // Use valid namespace formats + ctx1 := request.WithNamespace(context.Background(), "default") + ctx2 := request.WithNamespace(context.Background(), "org-2") + + idx1 := cached.Get(ctx1) + idx2 := cached.Get(ctx2) + + assert.Equal(t, "org1-ds", idx1.GetDefault().UID, "org 1 should get org-1 datasources") + assert.Equal(t, "org2-ds", idx2.GetDefault().UID, "org 2 should get org-2 datasources") + + // Subsequent calls should still return correct data + idx1Again := cached.Get(ctx1) + idx2Again := cached.Get(ctx2) + + assert.Equal(t, "org1-ds", idx1Again.GetDefault().UID, "org 1 should still get org-1 datasources from cache") + assert.Equal(t, "org2-ds", idx2Again.GetDefault().UID, "org 2 should still get org-2 datasources from cache") +} + +// TestCachedProvider_PreloadMultipleNamespaces verifies preloading multiple namespaces +func TestCachedProvider_PreloadMultipleNamespaces(t *testing.T) { + // Provider that returns different data based on namespace + underlying := &namespaceAwareProvider{ + datasourcesByNamespace: map[string][]DataSourceInfo{ + "default": {{UID: "org1-ds", Type: "prometheus", Name: "Org1 DS", Default: true}}, + "org-2": {{UID: "org2-ds", Type: "loki", Name: "Org2 DS", Default: true}}, + "org-3": {{UID: "org3-ds", Type: "tempo", Name: "Org3 DS", Default: true}}, + }, + } + cached := newCachedProvider(underlying.Index, defaultCacheSize, time.Minute) + + // Preload multiple namespaces + nsInfos := []authlib.NamespaceInfo{ + createNamespaceInfo(1, 0, "default"), + createNamespaceInfo(2, 0, "org-2"), + createNamespaceInfo(3, 0, "org-3"), + } + cached.Preload(context.Background(), nsInfos) + + // After preload, the underlying provider should have been called once per namespace + assert.Equal(t, 3, underlying.callCount, "preload should call underlying provider once per namespace") + + // Access all namespaces - should use preloaded data and get correct data per namespace + expectedUIDs := map[string]string{ + "default": "org1-ds", + "org-2": "org2-ds", + "org-3": "org3-ds", + } + + for _, ns := range []string{"default", "org-2", "org-3"} { + ctx := request.WithNamespace(context.Background(), ns) + idx := cached.Get(ctx) + require.NotNil(t, idx, "index for namespace %s should not be nil", ns) + assert.Equal(t, expectedUIDs[ns], idx.GetDefault().UID, "namespace %s should get correct datasource", ns) + } + + // The underlying provider should still have been called only 3 times (from preload) + assert.Equal(t, 3, underlying.callCount, + "access after preload should use cached data for all namespaces") +} + +// namespaceAwareProvider returns different datasources based on namespace +type namespaceAwareProvider struct { + datasourcesByNamespace map[string][]DataSourceInfo + callCount int +} + +func (p *namespaceAwareProvider) Index(ctx context.Context) *DatasourceIndex { + p.callCount++ + ns := request.NamespaceValue(ctx) + if ds, ok := p.datasourcesByNamespace[ns]; ok { + return NewDatasourceIndex(ds) + } + return NewDatasourceIndex(nil) +} + +// createNamespaceInfo creates a NamespaceInfo for testing +func createNamespaceInfo(orgID, stackID int64, value string) authlib.NamespaceInfo { + return authlib.NamespaceInfo{ + OrgID: orgID, + StackID: stackID, + Value: value, + } +} + +// Test DatasourceIndex functionality +func TestDatasourceIndex_Lookup(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds-uid-1", Type: "prometheus", Name: "Prometheus DS", Default: true, APIVersion: "v1"}, + {UID: "ds-uid-2", Type: "loki", Name: "Loki DS", Default: false, APIVersion: "v1"}, + } + idx := NewDatasourceIndex(datasources) + + t.Run("lookup by name", func(t *testing.T) { + ds := idx.Lookup("Prometheus DS") + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-1", ds.UID) + }) + + t.Run("lookup by UID", func(t *testing.T) { + ds := idx.Lookup("ds-uid-2") + require.NotNil(t, ds) + assert.Equal(t, "Loki DS", ds.Name) + }) + + t.Run("lookup unknown returns nil", func(t *testing.T) { + ds := idx.Lookup("unknown") + assert.Nil(t, ds) + }) + + t.Run("get default", func(t *testing.T) { + ds := idx.GetDefault() + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-1", ds.UID) + }) + + t.Run("lookup by UID directly", func(t *testing.T) { + ds := idx.LookupByUID("ds-uid-1") + require.NotNil(t, ds) + assert.Equal(t, "Prometheus DS", ds.Name) + }) + + t.Run("lookup by name directly", func(t *testing.T) { + ds := idx.LookupByName("Loki DS") + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-2", ds.UID) + }) +} + +func TestDatasourceIndex_EmptyIndex(t *testing.T) { + idx := NewDatasourceIndex(nil) + + assert.Nil(t, idx.GetDefault()) + assert.Nil(t, idx.Lookup("anything")) + assert.Nil(t, idx.LookupByUID("anything")) + assert.Nil(t, idx.LookupByName("anything")) +} + +// TestCachedProvider_TTLExpiration verifies that cache expires after TTL +func TestCachedProvider_TTLExpiration(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + // Use a very short TTL for testing + shortTTL := 50 * time.Millisecond + cached := newCachedProvider(underlying.get, defaultCacheSize, shortTTL) + + ctx := request.WithNamespace(context.Background(), "default") + + // First call - should call underlying provider + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first call should invoke underlying provider") + + // Second call immediately - should use cache + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(1), underlying.getCallCount(), "second call should use cache") + + // Wait for TTL to expire + time.Sleep(shortTTL + 20*time.Millisecond) + + // Third call after TTL - should call underlying provider again + idx3 := cached.Get(ctx) + require.NotNil(t, idx3) + assert.Equal(t, int64(2), underlying.getCallCount(), + "after TTL expiration, underlying provider should be called again") +} + +// TestCachedProvider_ParallelNamespacesFetch verifies that different namespaces can fetch in parallel +func TestCachedProvider_ParallelNamespacesFetch(t *testing.T) { + // Create a blocking provider that tracks concurrent executions + provider := &blockingProvider{ + blockDuration: 100 * time.Millisecond, + datasources: []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + }, + } + cached := newCachedProvider(provider.get, defaultCacheSize, time.Minute) + + numNamespaces := 5 + var wg sync.WaitGroup + + // Launch fetches for different namespaces simultaneously + startTime := time.Now() + for i := 0; i < numNamespaces; i++ { + wg.Add(1) + namespace := fmt.Sprintf("org-%d", i+1) + go func(ns string) { + defer wg.Done() + ctx := request.WithNamespace(context.Background(), ns) + idx := cached.Get(ctx) + require.NotNil(t, idx) + }(namespace) + } + wg.Wait() + elapsed := time.Since(startTime) + + // Verify that all namespaces were called + assert.Equal(t, int64(numNamespaces), provider.callCount.Load()) + + // Verify max concurrent executions shows parallelism + maxConcurrent := provider.maxConcurrent.Load() + assert.Equal(t, int64(numNamespaces), maxConcurrent) + + // If all namespaces had to wait sequentially, it would take numNamespaces * blockDuration + // With parallelism, it should be much faster (close to just blockDuration) + sequentialTime := time.Duration(numNamespaces) * provider.blockDuration + assert.Less(t, elapsed, sequentialTime) +} + +// TestCachedProvider_SameNamespaceSerialFetch verifies that the same namespace doesn't fetch concurrently +func TestCachedProvider_SameNamespaceSerialFetch(t *testing.T) { + // Create a blocking provider that tracks concurrent executions + provider := &blockingProvider{ + blockDuration: 100 * time.Millisecond, + datasources: []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + }, + } + cached := newCachedProvider(provider.get, defaultCacheSize, time.Minute) + + numGoroutines := 10 + var wg sync.WaitGroup + + // Launch multiple fetches for the SAME namespace simultaneously + ctx := request.WithNamespace(context.Background(), "default") + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }() + } + wg.Wait() + + // Max concurrent should be 1 since all goroutines are for the same namespace + maxConcurrent := provider.maxConcurrent.Load() + assert.Equal(t, int64(1), maxConcurrent) +} + +// blockingProvider is a test provider that simulates slow fetch operations +// and tracks concurrent executions +type blockingProvider struct { + blockDuration time.Duration + datasources []DataSourceInfo + callCount atomic.Int64 + currentActive atomic.Int64 + maxConcurrent atomic.Int64 +} + +func (p *blockingProvider) get(_ context.Context) any { + p.callCount.Add(1) + + // Track concurrent executions + current := p.currentActive.Add(1) + + // Update max concurrent if this is a new peak + for { + maxVal := p.maxConcurrent.Load() + if current <= maxVal { + break + } + if p.maxConcurrent.CompareAndSwap(maxVal, current) { + break + } + } + + // Simulate slow operation + time.Sleep(p.blockDuration) + + p.currentActive.Add(-1) + return p.datasources +} diff --git a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go index 18bc2fc17c4..c7215495dc9 100644 --- a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go @@ -2,7 +2,6 @@ package schemaversion import ( "context" - "sync" "time" ) @@ -11,65 +10,41 @@ import ( // string names/UIDs to structured reference objects with uid, type, and apiVersion. // cachedIndexProvider wraps a DataSourceIndexProvider with time-based caching. -// This prevents multiple DB queries and index builds during operations that may call -// provider.Index() multiple times (e.g., dashboard conversions with many datasource lookups). -// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton -// while still refreshing periodically. -// -// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. type cachedIndexProvider struct { - provider DataSourceIndexProvider - mu sync.RWMutex - index *DatasourceIndex - cachedAt time.Time - cacheTTL time.Duration + *cachedProvider[*DatasourceIndex] } -// Index returns the cached index if it's still valid (< 10s old), otherwise rebuilds it. -// Uses RWMutex for efficient concurrent reads when cache is valid. +// Index returns the cached index if it's still valid (< TTL old), otherwise rebuilds it. func (p *cachedIndexProvider) Index(ctx context.Context) *DatasourceIndex { - // Fast path: check if cache is still valid using read lock - p.mu.RLock() - if p.index != nil && time.Since(p.cachedAt) < p.cacheTTL { - idx := p.index - p.mu.RUnlock() - return idx - } - p.mu.RUnlock() - - // Slow path: cache expired or not yet built, acquire write lock - p.mu.Lock() - defer p.mu.Unlock() - - // Double-check: another goroutine might have refreshed the cache - // while we were waiting for the write lock - if p.index != nil && time.Since(p.cachedAt) < p.cacheTTL { - return p.index - } - - // Rebuild the cache - p.index = p.provider.Index(ctx) - p.cachedAt = time.Now() - return p.index + return p.Get(ctx) } -// WrapIndexProviderWithCache wraps a provider to cache the index with a 10-second TTL. -// Useful for conversions or migrations that may call provider.Index() multiple times. -// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton -// at the top level of dependency injection while still refreshing periodically. -// -// Example usage in dashboard conversion: -// -// cachedDsIndexProvider := schemaversion.WrapIndexProviderWithCache(dsIndexProvider) -// // Now all calls to cachedDsIndexProvider.Index(ctx) return the same cached index -// // for up to 10 seconds before refreshing -func WrapIndexProviderWithCache(provider DataSourceIndexProvider) DataSourceIndexProvider { - if provider == nil { - return nil +// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. +type cachedLibraryElementProvider struct { + *cachedProvider[[]LibraryElementInfo] +} + +func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { + return p.Get(ctx) +} + +// WrapIndexProviderWithCache wraps a DataSourceIndexProvider to cache indexes with a configurable TTL. +func WrapIndexProviderWithCache(provider DataSourceIndexProvider, cacheTTL time.Duration) DataSourceIndexProvider { + if provider == nil || cacheTTL <= 0 { + return provider } return &cachedIndexProvider{ - provider: provider, - cacheTTL: 10 * time.Second, + newCachedProvider[*DatasourceIndex](provider.Index, defaultCacheSize, cacheTTL), + } +} + +// WrapLibraryElementProviderWithCache wraps a LibraryElementIndexProvider to cache library elements with a configurable TTL. +func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider, cacheTTL time.Duration) LibraryElementIndexProvider { + if provider == nil || cacheTTL <= 0 { + return provider + } + return &cachedLibraryElementProvider{ + newCachedProvider[[]LibraryElementInfo](provider.GetLibraryElementInfo, defaultCacheSize, cacheTTL), } } @@ -216,60 +191,3 @@ func MigrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool, return nil } - -// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. -// This prevents multiple DB queries during operations that may call GetLibraryElementInfo() -// multiple times (e.g., dashboard conversions with many library panel lookups). -// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton -// while still refreshing periodically. -// -// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. -type cachedLibraryElementProvider struct { - provider LibraryElementIndexProvider - mu sync.RWMutex - elements []LibraryElementInfo - cachedAt time.Time - cacheTTL time.Duration -} - -// GetLibraryElementInfo returns the cached library elements if they're still valid (< 10s old), otherwise rebuilds the cache. -// Uses RWMutex for efficient concurrent reads when cache is valid. -func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { - // Fast path: check if cache is still valid using read lock - p.mu.RLock() - if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { - elements := p.elements - p.mu.RUnlock() - return elements - } - p.mu.RUnlock() - - // Slow path: cache expired or not yet built, acquire write lock - p.mu.Lock() - defer p.mu.Unlock() - - // Double-check: another goroutine might have refreshed the cache - // while we were waiting for the write lock - if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { - return p.elements - } - - // Rebuild the cache - p.elements = p.provider.GetLibraryElementInfo(ctx) - p.cachedAt = time.Now() - return p.elements -} - -// WrapLibraryElementProviderWithCache wraps a provider to cache library elements with a 10-second TTL. -// Useful for conversions or migrations that may call GetLibraryElementInfo() multiple times. -// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton -// at the top level of dependency injection while still refreshing periodically. -func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider) LibraryElementIndexProvider { - if provider == nil { - return nil - } - return &cachedLibraryElementProvider{ - provider: provider, - cacheTTL: 10 * time.Second, - } -} diff --git a/apps/dashboard/pkg/migration/schemaversion/migration_utils.go b/apps/dashboard/pkg/migration/schemaversion/migration_utils.go index 64dfc61589e..d964fcaf513 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migration_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/migration_utils.go @@ -102,3 +102,71 @@ func IsArray(value interface{}) bool { _, ok := value.([]interface{}) return ok } + +// AngularPanelMigrations maps deprecated Angular panel types to their modern equivalents. +// Used by both v0→v1 and v1→v2 conversions to ensure consistent migration behavior. +var AngularPanelMigrations = map[string]string{ + "table-old": "table", + "singlestat": "stat", + "grafana-singlestat-panel": "stat", + "grafana-piechart-panel": "piechart", + "grafana-worldmap-panel": "geomap", + "natel-discrete-panel": "state-timeline", +} + +// GetAngularPanelMigration checks if a panel type is an Angular panel and returns the new type to migrate to. +// Returns the new panel type if migration is needed, empty string otherwise. +// This handles both simple mappings and special cases like graph panel. +func GetAngularPanelMigration(panelType string, panel map[string]interface{}) string { + // Handle graph panel specially - it can migrate to different panel types + // based on xaxis.mode + if panelType == "graph" || panelType == "graphite" { + return GetGraphMigrationTarget(panel) + } + + // Check simple Angular panel mappings + if newType, isAngular := AngularPanelMigrations[panelType]; isAngular { + return newType + } + + return "" +} + +// GetGraphMigrationTarget determines the target panel type for graph panel migration. +// Graph panels can migrate to timeseries, barchart, bargauge, or histogram depending on xaxis.mode. +func GetGraphMigrationTarget(panel map[string]interface{}) string { + // Default to timeseries + newType := "timeseries" + + // Check xaxis mode for special cases + if xaxis, ok := panel["xaxis"].(map[string]interface{}); ok { + if mode, ok := xaxis["mode"].(string); ok { + switch mode { + case "series": + // Check legend values for bargauge vs barchart + if legend, ok := panel["legend"].(map[string]interface{}); ok { + if values, ok := legend["values"].(bool); ok && values { + newType = "bargauge" + } else { + newType = "barchart" + } + } else { + newType = "barchart" + } + case "histogram": + newType = "histogram" + } + } + } + + return newType +} + +// IsAngularPanelType checks if a panel type is a known Angular panel type. +func IsAngularPanelType(panelType string) bool { + if panelType == "graph" || panelType == "graphite" { + return true + } + _, isAngular := AngularPanelMigrations[panelType] + return isAngular +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/migrations/migrations.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/migrations/migrations.v42.json index 89ce80876d2..6ff361f144e 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/migrations/migrations.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/migrations/migrations.v42.json @@ -290,6 +290,7 @@ } }, { + "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", "datasource": { "type": "grafana-testdata-datasource" }, @@ -300,15 +301,7 @@ "y": 0 }, "id": 6, - "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" - }, + "mode": "markdown", "pluginVersion": "11.0.0-pre", "targets": [ { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json index 92a865b0b10..a705218273e 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json @@ -628,6 +628,20 @@ } ], "title": "Only nulls and no user set min \u0026 max", + "transformations": [ + { + "id": "convertFieldType", + "options": { + "conversions": [ + { + "destinationType": "number", + "targetField": "A-series" + } + ], + "fields": {} + } + } + ], "type": "gauge" }, { @@ -1179,4 +1193,4 @@ "title": "Panel Tests - Gauge", "uid": "_5rDmaQiz", "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 863d7b2a102..f24ec9a61a0 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 @@ -75,10 +75,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -154,10 +153,9 @@ "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -233,10 +231,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -305,85 +302,6 @@ "x": 12, "y": 1 }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 16, - "y": 1 - }, "id": 8, "maxDataPoints": 20, "options": { @@ -391,10 +309,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -460,8 +377,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 0, - "y": 7 + "x": 16, + "y": 1 }, "id": 22, "maxDataPoints": 20, @@ -470,10 +387,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -539,8 +455,8 @@ "gridPos": { "h": 6, "w": 4, - "x": 4, - "y": 7 + "x": 20, + "y": 1 }, "id": 23, "maxDataPoints": 20, @@ -549,10 +465,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -593,7 +508,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -630,9 +545,9 @@ }, "gridPos": { "h": 6, - "w": 5, + "w": 4, "x": 0, - "y": 14 + "y": 8 }, "id": 18, "maxDataPoints": 20, @@ -641,10 +556,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -709,9 +623,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 5, - "y": 14 + "w": 4, + "x": 4, + "y": 8 }, "id": 19, "maxDataPoints": 20, @@ -720,10 +634,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -788,9 +701,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 10, - "y": 14 + "w": 4, + "x": 8, + "y": 8 }, "id": 20, "maxDataPoints": 20, @@ -799,10 +712,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -867,9 +779,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 15, - "y": 14 + "w": 4, + "x": 12, + "y": 8 }, "id": 21, "maxDataPoints": 20, @@ -878,10 +790,9 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", + "barShape": "rounded", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -922,7 +833,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -963,9 +874,9 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 4, "x": 0, - "y": 21 + "y": 15 }, "id": 25, "maxDataPoints": 20, @@ -974,10 +885,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1042,9 +952,9 @@ }, "gridPos": { "h": 6, - "w": 6, - "x": 6, - "y": 21 + "w": 4, + "x": 4, + "y": 15 }, "id": 26, "maxDataPoints": 20, @@ -1053,10 +963,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1121,9 +1030,9 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 12, - "y": 21 + "w": 4, + "x": 8, + "y": 15 }, "id": 29, "maxDataPoints": 20, @@ -1132,10 +1041,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1199,10 +1107,10 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 0, - "y": 27 + "h": 6, + "w": 4, + "x": 12, + "y": 15 }, "id": 30, "maxDataPoints": 20, @@ -1211,10 +1119,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1278,10 +1185,10 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 6, - "y": 27 + "h": 6, + "w": 4, + "x": 16, + "y": 15 }, "id": 28, "maxDataPoints": 20, @@ -1290,10 +1197,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1330,7 +1236,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1377,7 +1283,7 @@ "h": 10, "w": 7, "x": 0, - "y": 35 + "y": 22 }, "id": 32, "maxDataPoints": 20, @@ -1386,10 +1292,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1460,7 +1365,7 @@ "h": 10, "w": 7, "x": 7, - "y": 35 + "y": 22 }, "id": 34, "maxDataPoints": 20, @@ -1469,10 +1374,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1543,7 +1447,7 @@ "h": 10, "w": 6, "x": 14, - "y": 35 + "y": 22 }, "id": 33, "maxDataPoints": 20, @@ -1552,10 +1456,9 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", + "barShape": "flat", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1592,7 +1495,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1603,7 +1506,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1634,21 +1536,21 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, "options": { "barWidth": 12, "barWidthFactor": 0.4, + "barShape": "rounded", "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1662,8 +1564,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1671,7 +1572,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1689,7 +1589,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1720,7 +1619,7 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, @@ -1730,11 +1629,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1748,8 +1646,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1757,7 +1654,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1777,7 +1673,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1788,7 +1684,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1820,7 +1715,7 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, @@ -1830,11 +1725,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1848,8 +1742,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1857,7 +1750,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 8, "min": 1, "noise": 2, @@ -1868,6 +1760,22 @@ "startValue": 0 } ], + "transformations": [ + { + "id": "calculateField", + "options": { + "mode": "unary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true, + "unary": { + "operator": "round", + "fieldName": "A-series" + } + } + } + ], "title": "Active gateways", "type": "radialbar" }, @@ -1875,7 +1783,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1907,7 +1814,7 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, @@ -1917,12 +1824,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1936,8 +1841,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1945,7 +1849,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 12, "min": 1, "noise": 2, @@ -1956,6 +1859,22 @@ "startValue": 0 } ], + "transformations": [ + { + "id": "calculateField", + "options": { + "mode": "unary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true, + "unary": { + "operator": "round", + "fieldName": "A-series" + } + } + } + ], "title": "Active pods", "type": "radialbar" }, @@ -1963,7 +1882,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1994,7 +1912,7 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, @@ -2004,11 +1922,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2022,8 +1939,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2031,7 +1947,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2049,7 +1964,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2080,7 +1994,7 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, @@ -2090,11 +2004,10 @@ "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "barShape": "rounded", "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2108,8 +2021,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2117,7 +2029,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2130,6 +2041,149 @@ ], "title": "Backend", "type": "radialbar" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 35, + "panels": [], + "title": "Empty data", + "type": "row" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 36, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true + }, + "barShape": "flat", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 0 + } + ], + "title": "Numeric, no series", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 37, + "options": { + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true + }, + "barShape": "flat", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "logs" + } + ], + "title": "Non-numeric", + "type": "gauge" } ], "preload": false, @@ -2147,4 +2201,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json index d4dd7980100..a75a58b7530 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json @@ -485,6 +485,7 @@ }, "id": 12, "options": { + "displayName": "My gauge", "minVizHeight": 75, "minVizWidth": 75, "orientation": "auto", @@ -955,10 +956,8 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1162,4 +1161,4 @@ "title": "Panel tests - Old gauge to new", "uid": "panel-tests-old-gauge-to-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v15.mimir_rollout_debugging.json b/apps/dashboard/pkg/migration/testdata/input/v15.mimir_rollout_debugging.json index dc0ca4e5845..1cc39838eab 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v15.mimir_rollout_debugging.json +++ b/apps/dashboard/pkg/migration/testdata/input/v15.mimir_rollout_debugging.json @@ -654,7 +654,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": true, "label": "cluster", @@ -677,7 +679,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": false, "label": "namespace", diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.mimir_rollout_debugging.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.mimir_rollout_debugging.v42.json index 6f507f5bc47..1195b2bec63 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.mimir_rollout_debugging.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.mimir_rollout_debugging.v42.json @@ -737,7 +737,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": true, "label": "cluster", @@ -758,7 +760,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": false, "label": "namespace", diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.mimir_rollout_debugging.v15.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.mimir_rollout_debugging.v15.json index dd9ec62ed85..30186560450 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.mimir_rollout_debugging.v15.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.mimir_rollout_debugging.v15.json @@ -717,7 +717,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": true, "label": "cluster", @@ -739,7 +741,9 @@ "text": "prod", "value": "prod" }, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "hide": 0, "includeAll": false, "label": "namespace", diff --git a/apps/example/go.mod b/apps/example/go.mod index d63d23be302..f2afba81283 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,17 +3,17 @@ module github.com/grafana/grafana/apps/example go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -24,13 +24,23 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -42,7 +52,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -56,43 +66,43 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/example/go.sum b/apps/example/go.sum index 70c83a9fe37..c5110f46829 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -25,14 +25,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -41,8 +67,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -56,10 +82,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= @@ -83,8 +109,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -116,10 +142,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -134,30 +160,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -175,34 +201,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -211,41 +237,41 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/example/kinds/manifest.cue b/apps/example/kinds/manifest.cue index 934d419623d..65947eaa151 100644 --- a/apps/example/kinds/manifest.cue +++ b/apps/example/kinds/manifest.cue @@ -34,7 +34,7 @@ manifest: { v0alpha1: { kinds: [examplev0alpha1] - // This is explicitly set to false to keep the example app disabled by default. + // 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 } @@ -48,14 +48,14 @@ v1alpha1: { // 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. + // 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, + // namespaced contains namespace-scoped resource routes for the version, // which are exposed as HTTP handlers on '/namespaces//'. namespaced: { "/something": { @@ -72,7 +72,7 @@ v1alpha1: { } } } - // cluster contains cluster-scoped resource routes for the version, + // cluster contains cluster-scoped resource routes for the version, // which are exposed as HTTP handlers on '/'. cluster: { "/other": { @@ -113,4 +113,4 @@ v1alpha1: { enabled: true } } -} \ No newline at end of file +} diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 476c6949c73..9a673364f89 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,10 +3,10 @@ module github.com/grafana/grafana/apps/folder go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( @@ -17,19 +17,29 @@ require ( 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-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/grafana/grafana-app-sdk/logging v0.48.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // 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/mailru/easyjson v0.9.1 // 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 @@ -40,29 +50,29 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index d8454185267..3b414d3e3fe 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -14,29 +14,55 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -58,8 +84,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -76,8 +102,6 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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= @@ -85,10 +109,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -101,16 +125,16 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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/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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -126,24 +150,24 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -154,31 +178,31 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 474779e0efe..d3f31d6f7a4 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,19 +52,19 @@ 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.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 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 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.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 @@ -131,7 +131,7 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bluele/gcache v0.0.2 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bufbuild/protocompile v0.14.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect @@ -143,7 +143,7 @@ require ( github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect 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/coreos/go-systemd/v22 v22.6.0 // 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 @@ -178,22 +178,24 @@ require ( github.com/go-logr/stdr v1.2.2 // 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/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // 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.22.0 // indirect github.com/go-openapi/strfmt v0.24.0 // indirect - github.com/go-openapi/swag v0.23.1 // 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/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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 @@ -212,7 +214,7 @@ require ( 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.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.9 // indirect @@ -221,7 +223,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // 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-20251204145817-de8c2bbf9eba // indirect + github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect @@ -285,7 +287,7 @@ 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/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -339,10 +341,10 @@ require ( github.com/pressly/goose/v3 v3.26.0 // indirect github.com/prometheus/alertmanager v0.28.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // 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/prometheus/procfs v0.19.2 // indirect github.com/prometheus/prometheus v0.303.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/redis/go-redis/v9 v9.14.0 // indirect @@ -359,7 +361,7 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -373,20 +375,20 @@ require ( github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // 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.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.38.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/google.golang.org/grpc/otelgrpc v0.64.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/instrumentation/net/http/otelhttp v0.64.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/exporters/jaeger v1.17.0 // indirect @@ -394,18 +396,18 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp 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/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.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.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 - go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.6.0 // indirect @@ -414,30 +416,30 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect gocloud.dev v0.43.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.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 gonum.org/v1/gonum v0.16.0 // 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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.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 @@ -446,20 +448,20 @@ require ( 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.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/component-base v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kms v0.34.2 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/kms v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.40.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/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 7f2fd8f462f..7e6806e89d0 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -2,8 +2,8 @@ buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff96.1/go.mod h1:pjl83IqpNF7Lm/lOPMLQ5IIfCg+yTu2A9OgYTcEncCs= buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1 h1:9nqE/pDc/HXAXiD5pZncPywjAzWgKuBkFFYgdK2lVU8= buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1/go.mod h1:1M7nlq2ljfzb95x9LaA2j1gYIvDkVZii58mGvTa9ExM= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -84,8 +84,8 @@ cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4 cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -150,6 +150,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 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/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= @@ -356,8 +358,8 @@ github.com/blugelabs/ice v1.0.0/go.mod h1:gNfFPk5zM+yxJROhthxhVQYjpBO9amuxWXJQ2L 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 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= @@ -377,10 +379,10 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY 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/centrifugal/centrifuge v0.37.2 h1:rerQNvDfYN2FZEkVtb/hvGV7SIrJfEQrKF3MaE8GDlo= -github.com/centrifugal/centrifuge v0.37.2/go.mod h1:aj4iRJGhzi3SlL8iUtVezxway1Xf8g+hmNQkLLO7sS8= -github.com/centrifugal/protocol v0.16.2 h1:KoIHgDeX1fFxyxQoKW+6E8ZTCf5mwGm8JyGoJ5NBMbQ= -github.com/centrifugal/protocol v0.16.2/go.mod h1:Q7OpS/8HMXDnL7f9DpNx24IhG96MP88WPpVTTCdrokI= +github.com/centrifugal/centrifuge v0.38.0 h1:UJTowwc5lSwnpvd3vbrTseODbU7osSggN67RTrJ8EfQ= +github.com/centrifugal/centrifuge v0.38.0/go.mod h1:rcZLARnO5GXOeE9qG7iIPMvERxESespqkSX4cGLCAzo= +github.com/centrifugal/protocol v0.17.0 h1:hD0WczyiG7zrVJcgkQsd5/nhfFXt0Y04SJHV2Z7B1rg= +github.com/centrifugal/protocol v0.17.0/go.mod h1:9MdiYyjw5Bw1+d5Sp4Y0NK+qiuTNyd88nrHJsUUh8k4= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -426,8 +428,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -499,8 +501,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m 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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/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= @@ -508,8 +510,8 @@ 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.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= 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= @@ -595,10 +597,10 @@ github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7Bd 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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= 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= @@ -607,28 +609,36 @@ github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0T 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -735,8 +745,8 @@ 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -817,8 +827,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.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-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= 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-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -835,10 +845,10 @@ 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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -853,6 +863,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= 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/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= 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= @@ -1100,8 +1112,8 @@ github.com/madflojo/testcerts v1.4.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLD 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU= github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -1349,8 +1361,8 @@ 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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= @@ -1362,23 +1374,25 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= 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/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0= github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/quagmt/udecimal v1.9.0 h1:TLuZiFeg0HhS6X8VDa78Y6XTaitZZfh+z5q4SXMzpDQ= +github.com/quagmt/udecimal v1.9.0/go.mod h1:ScmJ/xTGZcEoYiyMMzgDLn79PEJHcMBiJ4NNRT3FirA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 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/redis/rueidis v1.0.64 h1:XqgbueDuNV3qFdVdQwAHJl1uNt90zUuAJuzqjH4cw6Y= -github.com/redis/rueidis v1.0.64/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= +github.com/redis/rueidis v1.0.68 h1:gept0E45JGxVigWb3zoWHvxEc4IOC7kc4V/4XvN8eG8= +github.com/redis/rueidis v1.0.68/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 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= @@ -1416,8 +1430,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/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= 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= @@ -1444,8 +1458,8 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 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.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -1519,8 +1533,8 @@ github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2th github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= 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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= @@ -1551,15 +1565,15 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2 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.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= -go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -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/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= 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.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= -go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= 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= @@ -1591,19 +1605,19 @@ go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= 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= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= 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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -1614,12 +1628,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9 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/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= 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= @@ -1631,20 +1645,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7i 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.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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 v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 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/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= -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/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= @@ -1686,8 +1700,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -1701,8 +1715,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-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 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= @@ -1729,8 +1743,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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -1785,8 +1799,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -1809,8 +1823,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.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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= @@ -1823,8 +1837,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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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= @@ -1914,14 +1928,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -1931,8 +1945,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -1999,8 +2013,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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -2153,10 +2167,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 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= @@ -2207,8 +2221,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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -2219,8 +2233,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -2257,28 +2271,28 @@ 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.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= -k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-aggregator v0.34.2 h1:Nn0Vksj67WHBL2x7bJ6vuxL44RbMTK6uRtXX+3vMVJk= -k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q= -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= +k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE= +k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-aggregator v0.34.3 h1:rKsZWTD2As4dKuv+zzdJU0uo5H7bFlAEoSucai4mW6M= +k8s.io/kube-aggregator v0.34.3/go.mod h1:d4D8PV2FK4Qlq6u442FSum1tHPhK9tKdKBfH/A3R0I0= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/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= @@ -2309,10 +2323,10 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/iam/kinds/manifest.cue b/apps/iam/kinds/manifest.cue index c6f609cbe15..91f746c6a32 100644 --- a/apps/iam/kinds/manifest.cue +++ b/apps/iam/kinds/manifest.cue @@ -22,13 +22,40 @@ v0alpha1: { serviceaccountv0alpha1, externalGroupMappingv0alpha1 ] + routes: { namespaced: { + "/searchUsers": { + "GET": { + request: { + query: { + query?: string + limit?: int64 | 10 + offset?: int64 | 0 + page?: int64 | 1 + } + } + response: { + offset: int64 + totalHits: int64 + hits: [...#UserHit] + queryCost: float64 + maxScore: float64 + } + responseMetadata: { + typeMeta: false + objectMeta: false + } + } + } "/searchTeams": { "GET": { request: { query: { query?: string + limit?: int64 | 50 + offset?: int64 | 0 + page?: int64 | 1 } } response: { @@ -51,3 +78,15 @@ v0alpha1: { } } } + +#UserHit: { + name: string + title: string + login: string + email: string + role: string + lastSeenAt: int64 + lastSeenAtAge: string + provisioned: bool + score: float64 +} diff --git a/apps/iam/kinds/user.cue b/apps/iam/kinds/user.cue index a67c0949e1f..fc5bb8fb153 100644 --- a/apps/iam/kinds/user.cue +++ b/apps/iam/kinds/user.cue @@ -29,6 +29,9 @@ userv0alpha1: userKind & { // } schema: { spec: v0alpha1.UserSpec + status: { + lastSeenAt: int64 | 0 + } } // TODO: Uncomment when the custom routes implementation is done // routes: { diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go index 625cced10b9..9a99aadddee 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go @@ -23,6 +23,12 @@ type CoreRole struct { Spec CoreRoleSpec `json:"spec" yaml:"spec"` } +func NewCoreRole() *CoreRole { + return &CoreRole{ + Spec: *NewCoreRoleSpec(), + } +} + func (o *CoreRole) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go index 637f78355f8..82c0a8102e0 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaCoreRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &CoreRole{}, &CoreRoleList{}, resource.WithKind("CoreRole"), + schemaCoreRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewCoreRole(), &CoreRoleList{}, resource.WithKind("CoreRole"), resource.WithPlural("coreroles"), resource.WithScope(resource.NamespacedScope)) kindCoreRole = resource.Kind{ Schema: schemaCoreRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go index db20616c355..bbc7c2f65c4 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go @@ -23,6 +23,12 @@ type ExternalGroupMapping struct { Spec ExternalGroupMappingSpec `json:"spec" yaml:"spec"` } +func NewExternalGroupMapping() *ExternalGroupMapping { + return &ExternalGroupMapping{ + Spec: *NewExternalGroupMappingSpec(), + } +} + func (o *ExternalGroupMapping) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go index 91090a9b460..943c41d8e3e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaExternalGroupMapping = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ExternalGroupMapping{}, &ExternalGroupMappingList{}, resource.WithKind("ExternalGroupMapping"), + schemaExternalGroupMapping = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewExternalGroupMapping(), &ExternalGroupMappingList{}, resource.WithKind("ExternalGroupMapping"), resource.WithPlural("externalgroupmappings"), resource.WithScope(resource.NamespacedScope)) kindExternalGroupMapping = resource.Kind{ Schema: schemaExternalGroupMapping, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go index ffa5067c41b..d06550cb417 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go @@ -3,7 +3,10 @@ package v0alpha1 type GetSearchTeamsRequestParams struct { - Query *string `json:"query,omitempty"` + Query *string `json:"query,omitempty"` + Limit int64 `json:"limit,omitempty"` + Offset int64 `json:"offset,omitempty"` + Page int64 `json:"page,omitempty"` } // NewGetSearchTeamsRequestParams creates a new GetSearchTeamsRequestParams object. diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_object_gen.go new file mode 100644 index 00000000000..3a6c04a1fa0 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetSearchUsersRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetSearchUsersRequestParams `json:",inline"` +} + +func NewGetSearchUsersRequestParamsObject() *GetSearchUsersRequestParamsObject { + return &GetSearchUsersRequestParamsObject{} +} + +func (o *GetSearchUsersRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetSearchUsersRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSearchUsersRequestParamsObject) DeepCopyInto(dst *GetSearchUsersRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetSearchUsersRequestParams := GetSearchUsersRequestParams{} + _ = resource.CopyObjectInto(&dstGetSearchUsersRequestParams, &o.GetSearchUsersRequestParams) +} + +var _ runtime.Object = NewGetSearchUsersRequestParamsObject() diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_types_gen.go new file mode 100644 index 00000000000..22b73ea80d8 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_request_params_types_gen.go @@ -0,0 +1,15 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +type GetSearchUsersRequestParams struct { + Query *string `json:"query,omitempty"` + Limit int64 `json:"limit,omitempty"` + Offset int64 `json:"offset,omitempty"` + Page int64 `json:"page,omitempty"` +} + +// NewGetSearchUsersRequestParams creates a new GetSearchUsersRequestParams object. +func NewGetSearchUsersRequestParams() *GetSearchUsersRequestParams { + return &GetSearchUsersRequestParams{} +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_response_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_response_types_gen.go new file mode 100644 index 00000000000..d56cbfa3a3c --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchusers_response_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type UserHit struct { + Name string `json:"name"` + Title string `json:"title"` + Login string `json:"login"` + Email string `json:"email"` + Role string `json:"role"` + LastSeenAt int64 `json:"lastSeenAt"` + LastSeenAtAge string `json:"lastSeenAtAge"` + Provisioned bool `json:"provisioned"` + Score float64 `json:"score"` +} + +// NewUserHit creates a new UserHit object. +func NewUserHit() *UserHit { + return &UserHit{} +} + +// +k8s:openapi-gen=true +type GetSearchUsers struct { + Offset int64 `json:"offset"` + TotalHits int64 `json:"totalHits"` + Hits []UserHit `json:"hits"` + QueryCost float64 `json:"queryCost"` + MaxScore float64 `json:"maxScore"` +} + +// NewGetSearchUsers creates a new GetSearchUsers object. +func NewGetSearchUsers() *GetSearchUsers { + return &GetSearchUsers{ + Hits: []UserHit{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go index 27165fe70bd..66ad21c83cf 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go @@ -23,6 +23,12 @@ type GlobalRole struct { Spec GlobalRoleSpec `json:"spec" yaml:"spec"` } +func NewGlobalRole() *GlobalRole { + return &GlobalRole{ + Spec: *NewGlobalRoleSpec(), + } +} + func (o *GlobalRole) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go index ce3322f97c0..be220cb0e95 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaGlobalRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &GlobalRole{}, &GlobalRoleList{}, resource.WithKind("GlobalRole"), + schemaGlobalRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewGlobalRole(), &GlobalRoleList{}, resource.WithKind("GlobalRole"), resource.WithPlural("globalroles"), resource.WithScope(resource.NamespacedScope)) kindGlobalRole = resource.Kind{ Schema: schemaGlobalRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go index 3bd4609d25d..bb8b0644f62 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go @@ -23,6 +23,12 @@ type GlobalRoleBinding struct { Spec GlobalRoleBindingSpec `json:"spec" yaml:"spec"` } +func NewGlobalRoleBinding() *GlobalRoleBinding { + return &GlobalRoleBinding{ + Spec: *NewGlobalRoleBindingSpec(), + } +} + func (o *GlobalRoleBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go index 9b4e65aa5e6..2e39da946f9 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaGlobalRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &GlobalRoleBinding{}, &GlobalRoleBindingList{}, resource.WithKind("GlobalRoleBinding"), + schemaGlobalRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewGlobalRoleBinding(), &GlobalRoleBindingList{}, resource.WithKind("GlobalRoleBinding"), resource.WithPlural("globalrolebindings"), resource.WithScope(resource.NamespacedScope)) kindGlobalRoleBinding = resource.Kind{ Schema: schemaGlobalRoleBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go index 996beb7e002..8cc4c28c209 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go @@ -23,6 +23,12 @@ type ResourcePermission struct { Spec ResourcePermissionSpec `json:"spec" yaml:"spec"` } +func NewResourcePermission() *ResourcePermission { + return &ResourcePermission{ + Spec: *NewResourcePermissionSpec(), + } +} + func (o *ResourcePermission) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go index 257255a58fe..aa709f0d42a 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaResourcePermission = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ResourcePermission{}, &ResourcePermissionList{}, resource.WithKind("ResourcePermission"), + schemaResourcePermission = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewResourcePermission(), &ResourcePermissionList{}, resource.WithKind("ResourcePermission"), resource.WithPlural("resourcepermissions"), resource.WithScope(resource.NamespacedScope)) kindResourcePermission = resource.Kind{ Schema: schemaResourcePermission, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go index 20bb587157e..0673c1a17c5 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go @@ -23,6 +23,12 @@ type Role struct { Spec RoleSpec `json:"spec" yaml:"spec"` } +func NewRole() *Role { + return &Role{ + Spec: *NewRoleSpec(), + } +} + func (o *Role) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go index 74cc8730026..3aacfa2e060 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &Role{}, &RoleList{}, resource.WithKind("Role"), + schemaRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewRole(), &RoleList{}, resource.WithKind("Role"), resource.WithPlural("roles"), resource.WithScope(resource.NamespacedScope)) kindRole = resource.Kind{ Schema: schemaRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go index dfd7741e05a..996fdc3df65 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go @@ -23,6 +23,12 @@ type RoleBinding struct { Spec RoleBindingSpec `json:"spec" yaml:"spec"` } +func NewRoleBinding() *RoleBinding { + return &RoleBinding{ + Spec: *NewRoleBindingSpec(), + } +} + func (o *RoleBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go index abb2b3eddf9..0cf1d4dee8b 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &RoleBinding{}, &RoleBindingList{}, resource.WithKind("RoleBinding"), + schemaRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewRoleBinding(), &RoleBindingList{}, resource.WithKind("RoleBinding"), resource.WithPlural("rolebindings"), resource.WithScope(resource.NamespacedScope)) kindRoleBinding = resource.Kind{ Schema: schemaRoleBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go index fe3cd7f3609..e2cfc9f33a7 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go @@ -23,6 +23,12 @@ type ServiceAccount struct { Spec ServiceAccountSpec `json:"spec" yaml:"spec"` } +func NewServiceAccount() *ServiceAccount { + return &ServiceAccount{ + Spec: *NewServiceAccountSpec(), + } +} + func (o *ServiceAccount) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go index ea48c864739..71b171e0756 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaServiceAccount = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ServiceAccount{}, &ServiceAccountList{}, resource.WithKind("ServiceAccount"), + schemaServiceAccount = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewServiceAccount(), &ServiceAccountList{}, resource.WithKind("ServiceAccount"), resource.WithPlural("serviceaccounts"), resource.WithScope(resource.NamespacedScope)) kindServiceAccount = resource.Kind{ Schema: schemaServiceAccount, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go index 4030bebb9d1..85b57abf9d5 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go @@ -23,6 +23,12 @@ type Team struct { Spec TeamSpec `json:"spec" yaml:"spec"` } +func NewTeam() *Team { + return &Team{ + Spec: *NewTeamSpec(), + } +} + func (o *Team) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go index 7a4875cf2d5..299d846d10e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTeam = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &Team{}, &TeamList{}, resource.WithKind("Team"), + schemaTeam = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewTeam(), &TeamList{}, resource.WithKind("Team"), resource.WithPlural("teams"), resource.WithScope(resource.NamespacedScope)) kindTeam = resource.Kind{ Schema: schemaTeam, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go index a958c55f5e7..0d388192448 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go @@ -23,6 +23,12 @@ type TeamBinding struct { Spec TeamBindingSpec `json:"spec" yaml:"spec"` } +func NewTeamBinding() *TeamBinding { + return &TeamBinding{ + Spec: *NewTeamBindingSpec(), + } +} + func (o *TeamBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go index e3c2c11a8b5..d089b966e9b 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &TeamBinding{}, &TeamBindingList{}, resource.WithKind("TeamBinding"), + schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewTeamBinding(), &TeamBindingList{}, resource.WithKind("TeamBinding"), resource.WithPlural("teambindings"), resource.WithScope(resource.NamespacedScope)) kindTeamBinding = resource.Kind{ Schema: schemaTeamBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go index 665df84327e..bd7af9b3361 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type UserClient struct { @@ -75,6 +76,24 @@ func (c *UserClient) Patch(ctx context.Context, identifier resource.Identifier, return c.client.Patch(ctx, identifier, req, opts) } +func (c *UserClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus UserStatus, opts resource.UpdateOptions) (*User, error) { + return c.client.Update(ctx, &User{ + TypeMeta: metav1.TypeMeta{ + Kind: UserKind().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 *UserClient) 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/user_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go index 373112a1d87..f4bc19dccbb 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go @@ -21,6 +21,15 @@ type User struct { // Spec is the spec of the User Spec UserSpec `json:"spec" yaml:"spec"` + + Status UserStatus `json:"status" yaml:"status"` +} + +func NewUser() *User { + return &User{ + Spec: *NewUserSpec(), + Status: *NewUserStatus(), + } } func (o *User) GetSpec() any { @@ -37,11 +46,15 @@ func (o *User) SetSpec(spec any) error { } func (o *User) GetSubresources() map[string]any { - return map[string]any{} + return map[string]any{ + "status": o.Status, + } } func (o *User) GetSubresource(name string) (any, bool) { switch name { + case "status": + return o.Status, true default: return nil, false } @@ -49,6 +62,13 @@ func (o *User) GetSubresource(name string) (any, bool) { func (o *User) SetSubresource(name string, value any) error { switch name { + case "status": + cast, ok := value.(UserStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type UserStatus", value) + } + o.Status = cast + return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -220,6 +240,7 @@ func (o *User) DeepCopyInto(dst *User) { 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 @@ -291,3 +312,15 @@ func (s *UserSpec) DeepCopy() *UserSpec { func (s *UserSpec) DeepCopyInto(dst *UserSpec) { resource.CopyObjectInto(dst, s) } + +// DeepCopy creates a full deep copy of UserStatus +func (s *UserStatus) DeepCopy() *UserStatus { + cpy := &UserStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies UserStatus into another UserStatus object +func (s *UserStatus) DeepCopyInto(dst *UserStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go index a44d9dfcf2d..ba48b19c015 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaUser = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &User{}, &UserList{}, resource.WithKind("User"), + schemaUser = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewUser(), &UserList{}, resource.WithKind("User"), resource.WithPlural("users"), resource.WithScope(resource.NamespacedScope)) kindUser = resource.Kind{ Schema: schemaUser, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go index 34a138f59ce..dd0f3e46767 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go @@ -2,43 +2,12 @@ package v0alpha1 -// +k8s:openapi-gen=true -type UserstatusOperatorState 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 UserStatusOperatorStateState `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"` -} - -// NewUserstatusOperatorState creates a new UserstatusOperatorState object. -func NewUserstatusOperatorState() *UserstatusOperatorState { - return &UserstatusOperatorState{} -} - // +k8s:openapi-gen=true type UserStatus 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]UserstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` + LastSeenAt int64 `json:"lastSeenAt"` } // NewUserStatus creates a new UserStatus object. func NewUserStatus() *UserStatus { return &UserStatus{} } - -// +k8s:openapi-gen=true -type UserStatusOperatorStateState string - -const ( - UserStatusOperatorStateStateSuccess UserStatusOperatorStateState = "success" - UserStatusOperatorStateStateInProgress UserStatusOperatorStateState = "in_progress" - UserStatusOperatorStateStateFailed UserStatusOperatorStateState = "failed" -) 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 a41d51e8d70..87128d27699 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -1,8 +1,3 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Code generated by grafana-app-sdk. DO NOT EDIT. - package v0alpha1 import ( @@ -26,6 +21,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams": schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeamsBody": schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchUsers": schema_pkg_apis_iam_v0alpha1_GetSearchUsers(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), @@ -77,10 +73,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserHit": schema_pkg_apis_iam_v0alpha1_UserHit(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref), } @@ -693,6 +689,62 @@ func schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref common.ReferenceCallbac } } +func schema_pkg_apis_iam_v0alpha1_GetSearchUsers(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "offset": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "totalHits": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "hits": { + 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.UserHit"), + }, + }, + }, + }, + }, + "queryCost": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "maxScore": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + }, + Required: []string{"offset", "totalHits", "hits", "queryCost", "maxScore"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserHit"}, + } +} + func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2838,12 +2890,94 @@ func schema_pkg_apis_iam_v0alpha1_User(ref common.ReferenceCallback) common.Open Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec"), }, }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus"), + }, + }, }, - Required: []string{"metadata", "spec"}, + Required: []string{"metadata", "spec", "status"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_UserHit(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: "", + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "login": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "role": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "lastSeenAt": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "lastSeenAtAge": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "provisioned": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "score": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + }, + Required: []string{"name", "title", "login", "email", "role", "lastSeenAt", "lastSeenAtAge", "provisioned", "score"}, + }, + }, } } @@ -2970,90 +3104,15 @@ func schema_pkg_apis_iam_v0alpha1_UserStatus(ref common.ReferenceCallback) commo SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "operatorStates": { + "lastSeenAt": { 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/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState"), - }, - }, - }, - }, - }, - "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: "", - }, - }, - }, + Default: 0, + Type: []string{"integer"}, + Format: "int64", }, }, }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(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"}, + Required: []string{"lastSeenAt"}, }, }, } diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 5c27c228ed2..664c12226de 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -109,6 +109,13 @@ var appManifestData = app.ManifestData{ "items": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getGroupsExternalGroupMapping"), + }}, + }, }, }, "kind": { @@ -166,6 +173,36 @@ var appManifestData = app.ManifestData{ Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "limit", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "offset", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "page", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + { ParameterProps: spec3.ParameterProps{ Name: "query", @@ -200,6 +237,13 @@ var appManifestData = app.ManifestData{ "hits": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getSearchTeamsTeamHit"), + }}, + }, }, }, "kind": { @@ -247,6 +291,118 @@ var appManifestData = app.ManifestData{ }, }, }, + "/searchUsers": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getSearchUsers", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "limit", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "offset", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "page", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "query", + 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{ + "hits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getSearchUsersUserHit"), + }}, + }, + }, + }, + "maxScore": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + }, + }, + "offset": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + "queryCost": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + }, + }, + "totalHits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + }, + Required: []string{ + "offset", + "totalHits", + "hits", + "queryCost", + "maxScore", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, }, Cluster: map[string]spec3.PathProps{}, Schemas: map[string]spec.Schema{ @@ -289,6 +445,69 @@ var appManifestData = app.ManifestData{ }, }, }, + "getSearchUsersUserHit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "email": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "lastSeenAt": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + "lastSeenAtAge": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "login": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "provisioned": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "role": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "score": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "name", + "title", + "login", + "email", + "role", + "lastSeenAt", + "lastSeenAtAge", + "provisioned", + "score", + }, + }, + }, }, }, }, @@ -328,6 +547,7 @@ var customRouteToGoResponseType = map[string]any{ "v0alpha1|Team|groups|GET": v0alpha1.GetGroups{}, "v0alpha1||/searchTeams|GET": v0alpha1.GetSearchTeams{}, + "v0alpha1||/searchUsers|GET": v0alpha1.GetSearchUsers{}, } // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index 05216220d7e..ff2a0cc7feb 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" @@ -12,7 +14,6 @@ import ( foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/apps/iam/pkg/reconcilers" "github.com/grafana/grafana/pkg/services/authz" - "github.com/prometheus/client_golang/prometheus" ) var appManifestData = app.ManifestData{ @@ -78,7 +79,7 @@ func New(cfg app.Config) (app.App, error) { folderReconciler, err := reconcilers.NewFolderReconciler(reconcilers.ReconcilerConfig{ ZanzanaCfg: appSpecificConfig.ZanzanaClientCfg, Metrics: metrics, - }) + }, appSpecificConfig.MetricsRegisterer) if err != nil { return nil, fmt.Errorf("unable to create FolderReconciler: %w", err) } diff --git a/apps/iam/pkg/reconcilers/folder_reconciler.go b/apps/iam/pkg/reconcilers/folder_reconciler.go index 066637ee1e6..66a6868722d 100644 --- a/apps/iam/pkg/reconcilers/folder_reconciler.go +++ b/apps/iam/pkg/reconcilers/folder_reconciler.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" @@ -35,9 +36,9 @@ type FolderReconciler struct { metrics *ReconcilerMetrics } -func NewFolderReconciler(cfg ReconcilerConfig) (operator.Reconciler, error) { +func NewFolderReconciler(cfg ReconcilerConfig, reg prometheus.Registerer) (operator.Reconciler, error) { // Create Zanzana client - zanzanaClient, err := authz.NewRemoteZanzanaClient("*", cfg.ZanzanaCfg) + zanzanaClient, err := authz.NewRemoteZanzanaClient(cfg.ZanzanaCfg, reg) if err != nil { return nil, fmt.Errorf("unable to create zanzana client: %w", err) diff --git a/apps/investigations/Makefile b/apps/investigations/Makefile index aaac06377ca..bc8d6d30cb5 100644 --- a/apps/investigations/Makefile +++ b/apps/investigations/Makefile @@ -1,5 +1,10 @@ include ../sdk.mk -.PHONY: generate +.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 \ + --genoperatorstate=false \ + --defencoding=none \ No newline at end of file diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 3844fec2689..40677b56dab 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/apps/investigations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - k8s.io/apimachinery v0.34.2 + github.com/grafana/grafana-app-sdk v0.48.7 + k8s.io/apimachinery v0.34.3 k8s.io/klog/v2 v2.130.1 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -21,23 +21,33 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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/grafana-app-sdk/logging v0.48.3 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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/mailru/easyjson v0.9.1 // 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 @@ -50,42 +60,42 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/investigations/kinds/investigation.cue b/apps/investigations/kinds/investigation.cue index f1e9a69027f..1ade89416b9 100644 --- a/apps/investigations/kinds/investigation.cue +++ b/apps/investigations/kinds/investigation.cue @@ -1,39 +1,18 @@ package investigations -// This is our Investigation definition, which contains metadata about the kind, and the kind's schema -investigation: { +investigationV0alpha1: { kind: "Investigation" - group: "investigations.grafana.app" - apiResource: { - groupOverride: "investigations.grafana.app" - } pluralName: "Investigations" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - frontend: true - backend: true - options: { - generateObjectMeta: true - generateClient: true - k8sLike: true - package: "github.com/grafana/grafana/apps/investigations" - } - } - schema: { - // spec is the schema of our resource - spec: { - title: string - createdByProfile: #Person - hasCustomName: bool - isFavorite: bool - overviewNote: string - overviewNoteUpdatedAt: string - collectables: [...#Collectable] // +listType=atomic - viewMode: #ViewMode - } - } + schema: { + spec: { + title: string + createdByProfile: #Person + hasCustomName: bool + isFavorite: bool + overviewNote: string + overviewNoteUpdatedAt: string + collectables: [...#Collectable] // +listType=atomic + viewMode: #ViewMode } } } diff --git a/apps/investigations/kinds/investigationindex.cue b/apps/investigations/kinds/investigationindex.cue index 447e156be4f..65c5e3ae79f 100644 --- a/apps/investigations/kinds/investigationindex.cue +++ b/apps/investigations/kinds/investigationindex.cue @@ -1,37 +1,18 @@ package investigations -investigationIndex: { +investigationIndexV0alpha1:{ kind: "InvestigationIndex" - group: "investigations.grafana.app" - apiResource: { - groupOverride: "investigations.grafana.app" - } pluralName: "InvestigationIndexes" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - frontend: true - backend: true - options: { - generateObjectMeta: true - generateClient: true - k8sLike: true - package: "github.com/grafana/grafana/apps/investigations" - } - } - schema: { - spec: { - // Title of the index, e.g. 'Favorites' or 'My Investigations' - title: string + schema: { + spec: { + // Title of the index, e.g. 'Favorites' or 'My Investigations' + title: string - // The Person who owns this investigation index - owner: #Person + // The Person who owns this investigation index + owner: #Person - // Array of investigation summaries - investigationSummaries: [...#InvestigationSummary] // +listType=atomic - } - } + // Array of investigation summaries + investigationSummaries: [...#InvestigationSummary] // +listType=atomic } } } diff --git a/apps/investigations/kinds/manifest.cue b/apps/investigations/kinds/manifest.cue index d9e93b00061..947d84be482 100644 --- a/apps/investigations/kinds/manifest.cue +++ b/apps/investigations/kinds/manifest.cue @@ -3,8 +3,16 @@ package investigations manifest: { appName: "investigations" groupOverride: "investigations.grafana.app" - kinds: [ - investigation, - investigationIndex, - ] -} + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + investigationV0alpha1, + investigationIndexV0alpha1, + ] + } + } +} \ No newline at end of file diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go index dc73a0301e9..dea4d71802d 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_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 InvestigationClient struct { @@ -76,24 +75,6 @@ func (c *InvestigationClient) Patch(ctx context.Context, identifier resource.Ide return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationStatus, opts resource.UpdateOptions) (*Investigation, error) { - return c.client.Update(ctx, &Investigation{ - TypeMeta: metav1.TypeMeta{ - Kind: InvestigationKind().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 *InvestigationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_object_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_object_gen.go index eb819564176..f64d7b6e047 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_object_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_object_gen.go @@ -21,8 +21,6 @@ type Investigation struct { // Spec is the spec of the Investigation Spec InvestigationSpec `json:"spec" yaml:"spec"` - - Status InvestigationStatus `json:"status" yaml:"status"` } func (o *Investigation) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Investigation) SetSpec(spec any) error { } func (o *Investigation) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Investigation) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Investigation) GetSubresource(name string) (any, bool) { func (o *Investigation) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(InvestigationStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type InvestigationStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Investigation) DeepCopyInto(dst *Investigation) { 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 *InvestigationSpec) DeepCopy() *InvestigationSpec { func (s *InvestigationSpec) DeepCopyInto(dst *InvestigationSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of InvestigationStatus -func (s *InvestigationStatus) DeepCopy() *InvestigationStatus { - cpy := &InvestigationStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies InvestigationStatus into another InvestigationStatus object -func (s *InvestigationStatus) DeepCopyInto(dst *InvestigationStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_spec_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_spec_gen.go index d56124603a2..88d74be54eb 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_spec_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_spec_gen.go @@ -84,7 +84,6 @@ func NewInvestigationViewMode() *InvestigationViewMode { return &InvestigationViewMode{} } -// spec is the schema of our resource // +k8s:openapi-gen=true type InvestigationSpec struct { Title string `json:"title"` diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go index 573d743b3cf..dafe3e1012e 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_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 InvestigationIndexClient struct { @@ -76,24 +75,6 @@ func (c *InvestigationIndexClient) Patch(ctx context.Context, identifier resourc return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationIndexStatus, opts resource.UpdateOptions) (*InvestigationIndex, error) { - return c.client.Update(ctx, &InvestigationIndex{ - TypeMeta: metav1.TypeMeta{ - Kind: InvestigationIndexKind().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 *InvestigationIndexClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_object_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_object_gen.go index 67493201f05..b8272b4fae7 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_object_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_object_gen.go @@ -21,8 +21,6 @@ type InvestigationIndex struct { // Spec is the spec of the InvestigationIndex Spec InvestigationIndexSpec `json:"spec" yaml:"spec"` - - Status InvestigationIndexStatus `json:"status" yaml:"status"` } func (o *InvestigationIndex) GetSpec() any { @@ -39,15 +37,11 @@ func (o *InvestigationIndex) SetSpec(spec any) error { } func (o *InvestigationIndex) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *InvestigationIndex) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *InvestigationIndex) GetSubresource(name string) (any, bool) { func (o *InvestigationIndex) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(InvestigationIndexStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type InvestigationIndexStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *InvestigationIndex) DeepCopyInto(dst *InvestigationIndex) { 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 *InvestigationIndexSpec) DeepCopy() *InvestigationIndexSpec { func (s *InvestigationIndexSpec) DeepCopyInto(dst *InvestigationIndexSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of InvestigationIndexStatus -func (s *InvestigationIndexStatus) DeepCopy() *InvestigationIndexStatus { - cpy := &InvestigationIndexStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies InvestigationIndexStatus into another InvestigationIndexStatus object -func (s *InvestigationIndexStatus) DeepCopyInto(dst *InvestigationIndexStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/investigations/pkg/apis/investigations_manifest.go b/apps/investigations/pkg/apis/investigations_manifest.go index 2d3e9c7bdd3..249e9b49758 100644 --- a/apps/investigations/pkg/apis/investigations_manifest.go +++ b/apps/investigations/pkg/apis/investigations_manifest.go @@ -20,10 +20,10 @@ import ( ) var ( - rawSchemaInvestigationv0alpha1 = []byte(`{"Collectable":{"additionalProperties":false,"description":"Collectable represents an item collected during investigation","properties":{"createdAt":{"type":"string"},"datasource":{"$ref":"#/components/schemas/DatasourceRef"},"fieldConfig":{"type":"string"},"id":{"type":"string"},"logoPath":{"type":"string"},"note":{"type":"string"},"noteUpdatedAt":{"type":"string"},"origin":{"type":"string"},"queries":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"timeRange":{"$ref":"#/components/schemas/TimeRange"},"title":{"type":"string"},"type":{"type":"string"},"url":{"type":"string"}},"required":["id","createdAt","title","origin","type","queries","timeRange","datasource","url","note","noteUpdatedAt","fieldConfig"],"type":"object"},"DatasourceRef":{"additionalProperties":false,"description":"DatasourceRef is a reference to a datasource","properties":{"uid":{"type":"string"}},"required":["uid"],"type":"object"},"Investigation":{"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"},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"TimeRange":{"additionalProperties":false,"description":"TimeRange represents a time range with both absolute and relative values","properties":{"from":{"type":"string"},"raw":{"additionalProperties":false,"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"type":"object"},"to":{"type":"string"}},"required":["from","to","raw"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"description":"spec is the schema of our resource","properties":{"collectables":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Collectable"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","collectables","viewMode"],"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"}}`) + rawSchemaInvestigationv0alpha1 = []byte(`{"Collectable":{"additionalProperties":false,"description":"Collectable represents an item collected during investigation","properties":{"createdAt":{"type":"string"},"datasource":{"$ref":"#/components/schemas/DatasourceRef"},"fieldConfig":{"type":"string"},"id":{"type":"string"},"logoPath":{"type":"string"},"note":{"type":"string"},"noteUpdatedAt":{"type":"string"},"origin":{"type":"string"},"queries":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"timeRange":{"$ref":"#/components/schemas/TimeRange"},"title":{"type":"string"},"type":{"type":"string"},"url":{"type":"string"}},"required":["id","createdAt","title","origin","type","queries","timeRange","datasource","url","note","noteUpdatedAt","fieldConfig"],"type":"object"},"DatasourceRef":{"additionalProperties":false,"description":"DatasourceRef is a reference to a datasource","properties":{"uid":{"type":"string"}},"required":["uid"],"type":"object"},"Investigation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"TimeRange":{"additionalProperties":false,"description":"TimeRange represents a time range with both absolute and relative values","properties":{"from":{"type":"string"},"raw":{"additionalProperties":false,"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"type":"object"},"to":{"type":"string"}},"required":["from","to","raw"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"collectables":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Collectable"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","collectables","viewMode"],"type":"object"}}`) versionSchemaInvestigationv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaInvestigationv0alpha1, &versionSchemaInvestigationv0alpha1) - rawSchemaInvestigationIndexv0alpha1 = []byte(`{"CollectableSummary":{"additionalProperties":false,"properties":{"id":{"type":"string"},"logoPath":{"type":"string"},"origin":{"type":"string"},"title":{"type":"string"}},"required":["id","title","logoPath","origin"],"type":"object"},"InvestigationIndex":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"InvestigationSummary":{"additionalProperties":false,"description":"Type definition for investigation summaries","properties":{"collectableSummaries":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/CollectableSummary"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","viewMode","collectableSummaries"],"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"},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"investigationSummaries":{"description":"Array of investigation summaries\n+listType=atomic","items":{"$ref":"#/components/schemas/InvestigationSummary"},"type":"array"},"owner":{"$ref":"#/components/schemas/Person","description":"The Person who owns this investigation index"},"title":{"description":"Title of the index, e.g. 'Favorites' or 'My Investigations'","type":"string"}},"required":["title","owner","investigationSummaries"],"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"}}`) + rawSchemaInvestigationIndexv0alpha1 = []byte(`{"CollectableSummary":{"additionalProperties":false,"properties":{"id":{"type":"string"},"logoPath":{"type":"string"},"origin":{"type":"string"},"title":{"type":"string"}},"required":["id","title","logoPath","origin"],"type":"object"},"InvestigationIndex":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"InvestigationSummary":{"additionalProperties":false,"description":"Type definition for investigation summaries","properties":{"collectableSummaries":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/CollectableSummary"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","viewMode","collectableSummaries"],"type":"object"},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"investigationSummaries":{"description":"Array of investigation summaries\n+listType=atomic","items":{"$ref":"#/components/schemas/InvestigationSummary"},"type":"array"},"owner":{"$ref":"#/components/schemas/Person","description":"The Person who owns this investigation index"},"title":{"description":"Title of the index, e.g. 'Favorites' or 'My Investigations'","type":"string"}},"required":["title","owner","investigationSummaries"],"type":"object"}}`) versionSchemaInvestigationIndexv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaInvestigationIndexv0alpha1, &versionSchemaInvestigationIndexv0alpha1) ) diff --git a/apps/logsdrilldown/definitions/logsdrilldown-manifest.json b/apps/logsdrilldown/definitions/logsdrilldown-manifest.json new file mode 100644 index 00000000000..da34c0c70f2 --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldown-manifest.json @@ -0,0 +1,319 @@ +{ + "apiVersion": "apps.grafana.com/v1alpha2", + "kind": "AppManifest", + "metadata": { + "name": "logsdrilldown" + }, + "spec": { + "appName": "logsdrilldown", + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "kinds": [ + { + "kind": "LogsDrilldown", + "plural": "LogsDrilldowns", + "scope": "Namespaced", + "schemas": { + "LogsDrilldown": { + "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": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "conversion": false + }, + { + "kind": "LogsDrilldownDefaults", + "plural": "LogsDrilldownDefaults", + "scope": "Namespaced", + "schemas": { + "LogsDrilldownDefaults": { + "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": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "conversion": false + }, + { + "kind": "LogsDrilldownDefaultColumns", + "plural": "LogsDrilldownDefaultColumns", + "scope": "Namespaced", + "schemas": { + "LogsDefaultColumnsLabel": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["key", "value"], + "type": "object" + }, + "LogsDefaultColumnsLabels": { + "items": { + "$ref": "#/components/schemas/LogsDefaultColumnsLabel" + }, + "type": "array" + }, + "LogsDefaultColumnsRecord": { + "additionalProperties": false, + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labels": { + "$ref": "#/components/schemas/LogsDefaultColumnsLabels" + } + }, + "required": ["columns", "labels"], + "type": "object" + }, + "LogsDefaultColumnsRecords": { + "items": { + "$ref": "#/components/schemas/LogsDefaultColumnsRecord" + }, + "type": "array" + }, + "LogsDrilldownDefaultColumns": { + "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": { + "records": { + "$ref": "#/components/schemas/LogsDefaultColumnsRecords" + } + }, + "required": ["records"], + "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" + } + }, + "conversion": false + } + ] + } + ], + "preferredVersion": "v1alpha1" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..e0259f421ad --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json @@ -0,0 +1,92 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowns.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldown", + "plural": "logsdrilldowns" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..28aa314311d --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json @@ -0,0 +1,107 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowndefaultcolumns.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "records": { + "items": { + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labels": { + "items": { + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["key", "value"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["columns", "labels"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["records"], + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldownDefaultColumns", + "plural": "logsdrilldowndefaultcolumns" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..f2ab4b77e80 --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json @@ -0,0 +1,92 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowndefaults.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldownDefaults", + "plural": "logsdrilldowndefaults" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 2a35278d19a..e210773eb2e 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/apps/logsdrilldown go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -21,13 +21,23 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -36,7 +46,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -49,43 +59,43 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/logsdrilldown/kinds/logsdrilldown.cue b/apps/logsdrilldown/kinds/logsdrilldown.cue index 4a560ae3c6e..d2752103820 100644 --- a/apps/logsdrilldown/kinds/logsdrilldown.cue +++ b/apps/logsdrilldown/kinds/logsdrilldown.cue @@ -1,5 +1,9 @@ package kinds +import ( + "github.com/grafana/grafana/apps/logsdrilldown/kinds/v0alpha1" +) + LogsDrilldownSpecv1alpha1: { defaultFields: [...string] | *[] prettifyJSON: bool @@ -21,3 +25,12 @@ logsdrilldownDefaultsv1alpha1: { spec: LogsDrilldownSpecv1alpha1 } } + +// Default columns API +logsdrilldownDefaultColumnsv0alpha1: { + kind: "LogsDrilldownDefaultColumns" + pluralName: "LogsDrilldownDefaultColumns" + schema: { + spec: v0alpha1.LogsDefaultColumns + } +} diff --git a/apps/logsdrilldown/kinds/manifest.cue b/apps/logsdrilldown/kinds/manifest.cue index e2f2fb236b1..ab717de6a92 100644 --- a/apps/logsdrilldown/kinds/manifest.cue +++ b/apps/logsdrilldown/kinds/manifest.cue @@ -35,12 +35,12 @@ manifest: { // 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: [logsdrilldownv1alpha1, logsdrilldownDefaultsv1alpha1] + kinds: [logsdrilldownv1alpha1, logsdrilldownDefaultsv1alpha1, logsdrilldownDefaultColumnsv0alpha1] // [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 + 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. @@ -64,4 +64,4 @@ v1alpha1: { enabled: true } } -} \ No newline at end of file +} diff --git a/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue b/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue new file mode 100644 index 00000000000..123d5129c15 --- /dev/null +++ b/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue @@ -0,0 +1,19 @@ +package v0alpha1 + +#LogsDefaultColumnsLabel: { + key: string + value: string +} + +#LogsDefaultColumnsLabels: [...#LogsDefaultColumnsLabel] + +#LogsDefaultColumnsRecord: { + columns: [...string] + labels: #LogsDefaultColumnsLabels +} + +#LogsDefaultColumnsRecords: [...#LogsDefaultColumnsRecord] + +LogsDefaultColumns: { + records: #LogsDefaultColumnsRecords +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go index 4ede8bb4ee6..50bdc230cd1 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go @@ -25,6 +25,13 @@ type LogsDrilldown struct { Status LogsDrilldownStatus `json:"status" yaml:"status"` } +func NewLogsDrilldown() *LogsDrilldown { + return &LogsDrilldown{ + Spec: *NewLogsDrilldownSpec(), + Status: *NewLogsDrilldownStatus(), + } +} + func (o *LogsDrilldown) GetSpec() any { return o.Spec } diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go index e6864c965a7..8e88e870346 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldown{}, &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), + schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldown(), &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), resource.WithPlural("logsdrilldowns"), resource.WithScope(resource.NamespacedScope)) kindLogsDrilldown = resource.Kind{ Schema: schemaLogsDrilldown, diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go new file mode 100644 index 00000000000..b5d573bc1dc --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultColumnsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList] +} + +func NewLogsDrilldownDefaultColumnsClient(client resource.Client) *LogsDrilldownDefaultColumnsClient { + return &LogsDrilldownDefaultColumnsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList](client, LogsDrilldownDefaultColumnsKind()), + } +} + +func NewLogsDrilldownDefaultColumnsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultColumnsClient, error) { + c, err := generator.ClientFor(LogsDrilldownDefaultColumnsKind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultColumnsClient(c), nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaultColumns, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultColumnsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, 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 *LogsDrilldownDefaultColumnsClient) Create(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.CreateOptions) (*LogsDrilldownDefaultColumns, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = LogsDrilldownDefaultColumnsKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Update(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus LogsDrilldownDefaultColumnsStatus, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, &LogsDrilldownDefaultColumns{ + TypeMeta: metav1.TypeMeta{ + Kind: LogsDrilldownDefaultColumnsKind().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 *LogsDrilldownDefaultColumnsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go new file mode 100644 index 00000000000..311d2f02683 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_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" +) + +// LogsDrilldownDefaultColumnsJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type LogsDrilldownDefaultColumnsJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*LogsDrilldownDefaultColumnsJSONCodec) 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 (*LogsDrilldownDefaultColumnsJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &LogsDrilldownDefaultColumnsJSONCodec{} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go new file mode 100644 index 00000000000..a4bb052fe25 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_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 LogsDrilldownDefaultColumnsMetadata 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"` +} + +// NewLogsDrilldownDefaultColumnsMetadata creates a new LogsDrilldownDefaultColumnsMetadata object. +func NewLogsDrilldownDefaultColumnsMetadata() *LogsDrilldownDefaultColumnsMetadata { + return &LogsDrilldownDefaultColumnsMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go new file mode 100644 index 00000000000..4340a27714e --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go @@ -0,0 +1,326 @@ +// +// 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 LogsDrilldownDefaultColumns struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaultColumns + Spec LogsDrilldownDefaultColumnsSpec `json:"spec" yaml:"spec"` + + Status LogsDrilldownDefaultColumnsStatus `json:"status" yaml:"status"` +} + +func NewLogsDrilldownDefaultColumns() *LogsDrilldownDefaultColumns { + return &LogsDrilldownDefaultColumns{ + Spec: *NewLogsDrilldownDefaultColumnsSpec(), + Status: *NewLogsDrilldownDefaultColumnsStatus(), + } +} + +func (o *LogsDrilldownDefaultColumns) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error { + cast, ok := spec.(LogsDrilldownDefaultColumnsSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaultColumns) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(LogsDrilldownDefaultColumnsStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type LogsDrilldownDefaultColumnsStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaultColumns) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumns) DeepCopy() *LogsDrilldownDefaultColumns { + cpy := &LogsDrilldownDefaultColumns{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColumns) { + 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 = &LogsDrilldownDefaultColumns{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaultColumns `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumnsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultColumnsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaultColumns, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaultColumns); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) 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 *LogsDrilldownDefaultColumnsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaultColumns, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaultColumns) + } +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopy() *LogsDrilldownDefaultColumnsList { + cpy := &LogsDrilldownDefaultColumnsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyInto(dst *LogsDrilldownDefaultColumnsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultColumnsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *LogsDrilldownDefaultColumnsSpec) DeepCopy() *LogsDrilldownDefaultColumnsSpec { + cpy := &LogsDrilldownDefaultColumnsSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *LogsDrilldownDefaultColumnsSpec) DeepCopyInto(dst *LogsDrilldownDefaultColumnsSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of LogsDrilldownDefaultColumnsStatus +func (s *LogsDrilldownDefaultColumnsStatus) DeepCopy() *LogsDrilldownDefaultColumnsStatus { + cpy := &LogsDrilldownDefaultColumnsStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies LogsDrilldownDefaultColumnsStatus into another LogsDrilldownDefaultColumnsStatus object +func (s *LogsDrilldownDefaultColumnsStatus) DeepCopyInto(dst *LogsDrilldownDefaultColumnsStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go new file mode 100644 index 00000000000..cc5363e16bb --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_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 ( + schemaLogsDrilldownDefaultColumns = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldownDefaultColumns(), &LogsDrilldownDefaultColumnsList{}, resource.WithKind("LogsDrilldownDefaultColumns"), + resource.WithPlural("logsdrilldowndefaultcolumns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaultColumns = resource.Kind{ + Schema: schemaLogsDrilldownDefaultColumns, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &LogsDrilldownDefaultColumnsJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func LogsDrilldownDefaultColumnsKind() resource.Kind { + return kindLogsDrilldownDefaultColumns +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaultColumns +func LogsDrilldownDefaultColumnsSchema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaultColumns +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaultColumns diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go new file mode 100644 index 00000000000..ce12ebb0761 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go @@ -0,0 +1,43 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords []LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord struct { + Columns []string `json:"columns"` + Labels LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels `json:"labels"` +} + +// NewLogsDrilldownDefaultColumnsLogsDefaultColumnsRecord creates a new LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord object. +func NewLogsDrilldownDefaultColumnsLogsDefaultColumnsRecord() *LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord { + return &LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord{ + Columns: []string{}, + } +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels []LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// NewLogsDrilldownDefaultColumnsLogsDefaultColumnsLabel creates a new LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel object. +func NewLogsDrilldownDefaultColumnsLogsDefaultColumnsLabel() *LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel { + return &LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsSpec struct { + Records LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords `json:"records"` +} + +// NewLogsDrilldownDefaultColumnsSpec creates a new LogsDrilldownDefaultColumnsSpec object. +func NewLogsDrilldownDefaultColumnsSpec() *LogsDrilldownDefaultColumnsSpec { + return &LogsDrilldownDefaultColumnsSpec{} +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go new file mode 100644 index 00000000000..c2183832095 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsstatusOperatorState 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 LogsDrilldownDefaultColumnsStatusOperatorStateState `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"` +} + +// NewLogsDrilldownDefaultColumnsstatusOperatorState creates a new LogsDrilldownDefaultColumnsstatusOperatorState object. +func NewLogsDrilldownDefaultColumnsstatusOperatorState() *LogsDrilldownDefaultColumnsstatusOperatorState { + return &LogsDrilldownDefaultColumnsstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsStatus 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]LogsDrilldownDefaultColumnsstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewLogsDrilldownDefaultColumnsStatus creates a new LogsDrilldownDefaultColumnsStatus object. +func NewLogsDrilldownDefaultColumnsStatus() *LogsDrilldownDefaultColumnsStatus { + return &LogsDrilldownDefaultColumnsStatus{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsStatusOperatorStateState string + +const ( + LogsDrilldownDefaultColumnsStatusOperatorStateStateSuccess LogsDrilldownDefaultColumnsStatusOperatorStateState = "success" + LogsDrilldownDefaultColumnsStatusOperatorStateStateInProgress LogsDrilldownDefaultColumnsStatusOperatorStateState = "in_progress" + LogsDrilldownDefaultColumnsStatusOperatorStateStateFailed LogsDrilldownDefaultColumnsStatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go index 8dfb90a1bfb..ff5ce10adce 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go @@ -25,6 +25,13 @@ type LogsDrilldownDefaults struct { Status LogsDrilldownDefaultsStatus `json:"status" yaml:"status"` } +func NewLogsDrilldownDefaults() *LogsDrilldownDefaults { + return &LogsDrilldownDefaults{ + Spec: *NewLogsDrilldownDefaultsSpec(), + Status: *NewLogsDrilldownDefaultsStatus(), + } +} + func (o *LogsDrilldownDefaults) GetSpec() any { return o.Spec } diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go index 20e952ce021..f488c319db2 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaults{}, &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), + schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldownDefaults(), &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), resource.WithPlural("logsdrilldowndefaults"), resource.WithScope(resource.NamespacedScope)) kindLogsDrilldownDefaults = resource.Kind{ Schema: schemaLogsDrilldownDefaults, diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go b/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go index ef0d5801511..2350b924dda 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go @@ -20,12 +20,15 @@ import ( ) var ( - rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) - versionSchemaLogsDrilldownv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) - rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) - versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) + rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownDefaultColumnsv1alpha1 = []byte(`{"LogsDefaultColumnsLabel":{"additionalProperties":false,"properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"],"type":"object"},"LogsDefaultColumnsLabels":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsLabel"},"type":"array"},"LogsDefaultColumnsRecord":{"additionalProperties":false,"properties":{"columns":{"items":{"type":"string"},"type":"array"},"labels":{"$ref":"#/components/schemas/LogsDefaultColumnsLabels"}},"required":["columns","labels"],"type":"object"},"LogsDefaultColumnsRecords":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsRecord"},"type":"array"},"LogsDrilldownDefaultColumns":{"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":{"records":{"$ref":"#/components/schemas/LogsDefaultColumnsRecords"}},"required":["records"],"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"}}`) + versionSchemaLogsDrilldownDefaultColumnsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultColumnsv1alpha1, &versionSchemaLogsDrilldownDefaultColumnsv1alpha1) ) var appManifestData = app.ManifestData{ @@ -52,6 +55,14 @@ var appManifestData = app.ManifestData{ Conversion: false, Schema: &versionSchemaLogsDrilldownDefaultsv1alpha1, }, + + { + Kind: "LogsDrilldownDefaultColumns", + Plural: "LogsDrilldownDefaultColumns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultColumnsv1alpha1, + }, }, Routes: app.ManifestVersionRoutes{ Namespaced: map[string]spec3.PathProps{}, @@ -71,8 +82,9 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "LogsDrilldown/v1alpha1": v1alpha1.LogsDrilldownKind(), - "LogsDrilldownDefaults/v1alpha1": v1alpha1.LogsDrilldownDefaultsKind(), + "LogsDrilldown/v1alpha1": v1alpha1.LogsDrilldownKind(), + "LogsDrilldownDefaults/v1alpha1": v1alpha1.LogsDrilldownDefaultsKind(), + "LogsDrilldownDefaultColumns/v1alpha1": v1alpha1.LogsDrilldownDefaultColumnsKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/logsdrilldown/pkg/app/app.go b/apps/logsdrilldown/pkg/app/app.go index 7fda1ecb99f..23260270207 100644 --- a/apps/logsdrilldown/pkg/app/app.go +++ b/apps/logsdrilldown/pkg/app/app.go @@ -31,6 +31,9 @@ func New(cfg app.Config) (app.App, error) { { Kind: logsdrilldownv1alpha1.LogsDrilldownDefaultsKind(), }, + { + Kind: logsdrilldownv1alpha1.LogsDrilldownDefaultColumnsKind(), + }, }, } diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +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/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go new file mode 100644 index 00000000000..c133b65f45b --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownClient struct { + client *resource.TypedClient[*LogsDrilldown, *LogsDrilldownList] +} + +func NewLogsDrilldownClient(client resource.Client) *LogsDrilldownClient { + return &LogsDrilldownClient{ + client: resource.NewTypedClient[*LogsDrilldown, *LogsDrilldownList](client, Kind()), + } +} + +func NewLogsDrilldownClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownClient(c), nil +} + +func (c *LogsDrilldownClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldown, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownList, 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 *LogsDrilldownClient) Create(ctx context.Context, obj *LogsDrilldown, opts resource.CreateOptions) (*LogsDrilldown, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownClient) Update(ctx context.Context, obj *LogsDrilldown, opts resource.UpdateOptions) (*LogsDrilldown, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldown, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldown, error) { + return c.client.Update(ctx, &LogsDrilldown{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_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" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) 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 (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_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 Metadata 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"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go new file mode 100644 index 00000000000..5d40a873e6b --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go @@ -0,0 +1,319 @@ +// +// 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 LogsDrilldown struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldown + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldown) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldown) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldown) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldown) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldown) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldown) 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 *LogsDrilldown) 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 *LogsDrilldown) 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 *LogsDrilldown) 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 *LogsDrilldown) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldown) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldown) 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 *LogsDrilldown) 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 *LogsDrilldown) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldown) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldown) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldown) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldown) DeepCopy() *LogsDrilldown { + cpy := &LogsDrilldown{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldown) DeepCopyInto(dst *LogsDrilldown) { + 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 = &LogsDrilldown{} + +// +k8s:openapi-gen=true +type LogsDrilldownList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldown `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownList) Copy() resource.ListObject { + cpy := &LogsDrilldownList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldown, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldown); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownList) 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 *LogsDrilldownList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldown, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldown) + } +} + +func (o *LogsDrilldownList) DeepCopy() *LogsDrilldownList { + cpy := &LogsDrilldownList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownList) DeepCopyInto(dst *LogsDrilldownList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go new file mode 100644 index 00000000000..942794416e8 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_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 ( + schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldown{}, &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), + resource.WithPlural("logsdrilldowns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldown = resource.Kind{ + Schema: schemaLogsDrilldown, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldown +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldown +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldown +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldown diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go new file mode 100644 index 00000000000..faff5c108dd --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type Spec struct { + DefaultFields []string `json:"defaultFields"` + PrettifyJSON bool `json:"prettifyJSON"` + WrapLogMessage bool `json:"wrapLogMessage"` + InterceptDismissed bool `json:"interceptDismissed"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{ + DefaultFields: []string{}, + } +} diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go similarity index 50% rename from apps/investigations/pkg/apis/investigations/v0alpha1/investigation_status_gen.go rename to apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go index c70224b96e4..9b227b00f44 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_status_gen.go +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go @@ -1,44 +1,44 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v0alpha1 +package v1alpha1 // +k8s:openapi-gen=true -type InvestigationstatusOperatorState struct { +type StatusOperatorState 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 InvestigationStatusOperatorStateState `json:"state"` + State StatusOperatorStateState `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"` } -// NewInvestigationstatusOperatorState creates a new InvestigationstatusOperatorState object. -func NewInvestigationstatusOperatorState() *InvestigationstatusOperatorState { - return &InvestigationstatusOperatorState{} +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} } // +k8s:openapi-gen=true -type InvestigationStatus struct { +type Status 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]InvestigationstatusOperatorState `json:"operatorStates,omitempty"` + OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// NewInvestigationStatus creates a new InvestigationStatus object. -func NewInvestigationStatus() *InvestigationStatus { - return &InvestigationStatus{} +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} } // +k8s:openapi-gen=true -type InvestigationStatusOperatorStateState string +type StatusOperatorStateState string const ( - InvestigationStatusOperatorStateStateSuccess InvestigationStatusOperatorStateState = "success" - InvestigationStatusOperatorStateStateInProgress InvestigationStatusOperatorStateState = "in_progress" - InvestigationStatusOperatorStateStateFailed InvestigationStatusOperatorStateState = "failed" + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" ) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +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/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go new file mode 100644 index 00000000000..b66471eb4ba --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultColumnsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList] +} + +func NewLogsDrilldownDefaultColumnsClient(client resource.Client) *LogsDrilldownDefaultColumnsClient { + return &LogsDrilldownDefaultColumnsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList](client, Kind()), + } +} + +func NewLogsDrilldownDefaultColumnsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultColumnsClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultColumnsClient(c), nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaultColumns, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultColumnsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, 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 *LogsDrilldownDefaultColumnsClient) Create(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.CreateOptions) (*LogsDrilldownDefaultColumns, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Update(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, &LogsDrilldownDefaultColumns{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownDefaultColumnsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_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" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) 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 (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_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 Metadata 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"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go new file mode 100644 index 00000000000..3173c28330e --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go @@ -0,0 +1,319 @@ +// +// 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 LogsDrilldownDefaultColumns struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaultColumns + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldownDefaultColumns) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaultColumns) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) 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 *LogsDrilldownDefaultColumns) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaultColumns) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumns) DeepCopy() *LogsDrilldownDefaultColumns { + cpy := &LogsDrilldownDefaultColumns{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColumns) { + 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 = &LogsDrilldownDefaultColumns{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaultColumns `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumnsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultColumnsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaultColumns, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaultColumns); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) 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 *LogsDrilldownDefaultColumnsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaultColumns, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaultColumns) + } +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopy() *LogsDrilldownDefaultColumnsList { + cpy := &LogsDrilldownDefaultColumnsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyInto(dst *LogsDrilldownDefaultColumnsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultColumnsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go new file mode 100644 index 00000000000..b50be391fc7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_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 ( + schemaLogsDrilldownDefaultColumns = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaultColumns{}, &LogsDrilldownDefaultColumnsList{}, resource.WithKind("LogsDrilldownDefaultColumns"), + resource.WithPlural("logsdrilldowndefaultcolumns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaultColumns = resource.Kind{ + Schema: schemaLogsDrilldownDefaultColumns, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldownDefaultColumns +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaultColumns +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaultColumns +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaultColumns diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go new file mode 100644 index 00000000000..d9cd977aeb9 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go @@ -0,0 +1,43 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDefaultColumnsRecords []LogsDefaultColumnsRecord + +// +k8s:openapi-gen=true +type LogsDefaultColumnsRecord struct { + Columns []string `json:"columns"` + Labels LogsDefaultColumnsLabels `json:"labels"` +} + +// NewLogsDefaultColumnsRecord creates a new LogsDefaultColumnsRecord object. +func NewLogsDefaultColumnsRecord() *LogsDefaultColumnsRecord { + return &LogsDefaultColumnsRecord{ + Columns: []string{}, + } +} + +// +k8s:openapi-gen=true +type LogsDefaultColumnsLabels []LogsDefaultColumnsLabel + +// +k8s:openapi-gen=true +type LogsDefaultColumnsLabel struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// NewLogsDefaultColumnsLabel creates a new LogsDefaultColumnsLabel object. +func NewLogsDefaultColumnsLabel() *LogsDefaultColumnsLabel { + return &LogsDefaultColumnsLabel{} +} + +// +k8s:openapi-gen=true +type Spec struct { + Records LogsDefaultColumnsRecords `json:"records"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{} +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go new file mode 100644 index 00000000000..9b227b00f44 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StatusOperatorState 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 StatusOperatorStateState `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"` +} + +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} +} + +// +k8s:openapi-gen=true +type Status 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]StatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} +} + +// +k8s:openapi-gen=true +type StatusOperatorStateState string + +const ( + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +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/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go new file mode 100644 index 00000000000..cc06a10b1e7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaults, *LogsDrilldownDefaultsList] +} + +func NewLogsDrilldownDefaultsClient(client resource.Client) *LogsDrilldownDefaultsClient { + return &LogsDrilldownDefaultsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaults, *LogsDrilldownDefaultsList](client, Kind()), + } +} + +func NewLogsDrilldownDefaultsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultsClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultsClient(c), nil +} + +func (c *LogsDrilldownDefaultsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaults, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultsList, 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 *LogsDrilldownDefaultsClient) Create(ctx context.Context, obj *LogsDrilldownDefaults, opts resource.CreateOptions) (*LogsDrilldownDefaults, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultsClient) Update(ctx context.Context, obj *LogsDrilldownDefaults, opts resource.UpdateOptions) (*LogsDrilldownDefaults, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaults, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldownDefaults, error) { + return c.client.Update(ctx, &LogsDrilldownDefaults{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownDefaultsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_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" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) 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 (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_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 Metadata 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"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go new file mode 100644 index 00000000000..d9354522dd7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go @@ -0,0 +1,319 @@ +// +// 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 LogsDrilldownDefaults struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaults + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldownDefaults) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaults) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaults) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaults) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaults) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaults) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) 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 *LogsDrilldownDefaults) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaults) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaults) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaults) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaults) DeepCopy() *LogsDrilldownDefaults { + cpy := &LogsDrilldownDefaults{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaults) DeepCopyInto(dst *LogsDrilldownDefaults) { + 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 = &LogsDrilldownDefaults{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaults `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaults, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaults); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultsList) 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 *LogsDrilldownDefaultsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaults, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaults) + } +} + +func (o *LogsDrilldownDefaultsList) DeepCopy() *LogsDrilldownDefaultsList { + cpy := &LogsDrilldownDefaultsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultsList) DeepCopyInto(dst *LogsDrilldownDefaultsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_schema_gen.go new file mode 100644 index 00000000000..bda3e49377d --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_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 ( + schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaults{}, &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), + resource.WithPlural("logsdrilldowndefaults"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaults = resource.Kind{ + Schema: schemaLogsDrilldownDefaults, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldownDefaults +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaults +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaults +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaults diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go new file mode 100644 index 00000000000..faff5c108dd --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type Spec struct { + DefaultFields []string `json:"defaultFields"` + PrettifyJSON bool `json:"prettifyJSON"` + WrapLogMessage bool `json:"wrapLogMessage"` + InterceptDismissed bool `json:"interceptDismissed"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{ + DefaultFields: []string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go new file mode 100644 index 00000000000..9b227b00f44 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StatusOperatorState 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 StatusOperatorStateState `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"` +} + +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} +} + +// +k8s:openapi-gen=true +type Status 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]StatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} +} + +// +k8s:openapi-gen=true +type StatusOperatorStateState string + +const ( + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go b/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go new file mode 100644 index 00000000000..9deb5d5d3a1 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go @@ -0,0 +1,150 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +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" + + logsdrilldownv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1" + logsdrilldowndefaultcolumnsv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1" + logsdrilldowndefaultsv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1" +) + +var ( + rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) + rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"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":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownDefaultColumnsv1alpha1 = []byte(`{"LogsDefaultColumnsLabel":{"additionalProperties":false,"properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"],"type":"object"},"LogsDefaultColumnsLabels":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsLabel"},"type":"array"},"LogsDefaultColumnsRecord":{"additionalProperties":false,"properties":{"columns":{"items":{"type":"string"},"type":"array"},"labels":{"$ref":"#/components/schemas/LogsDefaultColumnsLabels"}},"required":["columns","labels"],"type":"object"},"LogsDefaultColumnsRecords":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsRecord"},"type":"array"},"LogsDrilldownDefaultColumns":{"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":{"records":{"$ref":"#/components/schemas/LogsDefaultColumnsRecords"}},"required":["records"],"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"}}`) + versionSchemaLogsDrilldownDefaultColumnsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultColumnsv1alpha1, &versionSchemaLogsDrilldownDefaultColumnsv1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "logsdrilldown", + Group: "logsdrilldown.grafana.app", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v1alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "LogsDrilldown", + Plural: "LogsDrilldowns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownv1alpha1, + }, + + { + Kind: "LogsDrilldownDefaults", + Plural: "LogsDrilldownDefaults", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultsv1alpha1, + }, + + { + Kind: "LogsDrilldownDefaultColumns", + Plural: "LogsDrilldownDefaultColumns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultColumnsv1alpha1, + }, + }, + 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("logsdrilldown") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "LogsDrilldown/v1alpha1": logsdrilldownv1alpha1.Kind(), + "LogsDrilldownDefaults/v1alpha1": logsdrilldowndefaultsv1alpha1.Kind(), + "LogsDrilldownDefaultColumns/v1alpha1": logsdrilldowndefaultcolumnsv1alpha1.Kind(), +} + +// 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/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.ts new file mode 100644 index 00000000000..f7ba7b0f223 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_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 LogsDrilldownDefaultColumns { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.metadata.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/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/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..fde99894776 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts @@ -0,0 +1,38 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export type LogsDefaultColumnsRecords = LogsDefaultColumnsRecord[]; + +export const defaultLogsDefaultColumnsRecords = (): LogsDefaultColumnsRecords => ([]); + +export interface LogsDefaultColumnsRecord { + columns: string[]; + labels: LogsDefaultColumnsLabels; +} + +export const defaultLogsDefaultColumnsRecord = (): LogsDefaultColumnsRecord => ({ + columns: [], + labels: defaultLogsDefaultColumnsLabels(), +}); + +export type LogsDefaultColumnsLabels = LogsDefaultColumnsLabel[]; + +export const defaultLogsDefaultColumnsLabels = (): LogsDefaultColumnsLabels => ([]); + +export interface LogsDefaultColumnsLabel { + key: string; + value: string; +} + +export const defaultLogsDefaultColumnsLabel = (): LogsDefaultColumnsLabel => ({ + key: "", + value: "", +}); + +export interface Spec { + records: LogsDefaultColumnsRecords; +} + +export const defaultSpec = (): Spec => ({ + records: defaultLogsDefaultColumnsRecords(), +}); + diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.status.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/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/playlist/go.mod b/apps/playlist/go.mod index ae38e3d0cd3..dd749f9fede 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,16 +3,16 @@ module github.com/grafana/grafana/apps/playlist go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - k8s.io/apimachinery v0.34.2 - k8s.io/client-go v0.34.2 + github.com/grafana/grafana-app-sdk v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/client-go v0.34.3 k8s.io/klog/v2 v2.130.1 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -22,23 +22,33 @@ require ( 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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/grafana-app-sdk/logging v0.48.3 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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/mailru/easyjson v0.9.1 // 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 @@ -51,41 +61,41 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 3df8e8b5ebe..e5233ba82c9 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -23,14 +23,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -39,8 +65,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -48,10 +74,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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,8 +99,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -95,8 +121,6 @@ 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= @@ -104,10 +128,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -122,30 +146,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -163,34 +187,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -199,39 +223,39 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/plugins/README.md b/apps/plugins/README.md new file mode 100644 index 00000000000..7f91dd6ea12 --- /dev/null +++ b/apps/plugins/README.md @@ -0,0 +1,20 @@ +# Plugins App + +API documentation is available at http://localhost:3000/swagger?api=plugins.grafana.app-v0alpha1 + +## Codegen + +- Go: `make generate` +- Frontend: Follow instructions in this [README](../..//packages/grafana-api-clients/README.md) + +## Plugin sync + +The plugin sync pushes the plugins loaded from disk to the plugins API. + +To enable, add these feature toggles in your `custom.ini`: + +```ini +[feature_toggles] +pluginInstallAPISync = true +pluginStoreServiceLoading = true +``` diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 669c7c46844..678d460910b 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -10,22 +10,21 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/emicklei/go-restful/v3 v3.13.0 - github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 - github.com/grafana/grafana/pkg/apimachinery v0.0.0 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/stretchr/testify v1.11.1 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/client-go v0.34.2 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/client-go v0.34.3 k8s.io/klog/v2 v2.130.1 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect @@ -37,16 +36,21 @@ require ( 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/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bluele/gcache v0.0.2 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bwmarrin/snowflake v0.3.0 // 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/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/coreos/go-systemd/v22 v22.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -58,10 +62,20 @@ require ( 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/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/gobwas/glob v0.2.3 // indirect @@ -73,17 +87,20 @@ require ( 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.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect + github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // 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.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect + github.com/grafana/grafana/pkg/apimachinery v0.0.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect + github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grafana/sqlds/v4 v4.2.7 // indirect @@ -113,7 +130,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -132,11 +149,15 @@ require ( 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 + github.com/nikunjy/rules v1.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 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.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 @@ -145,72 +166,76 @@ require ( github.com/prometheus/alertmanager v0.28.2 // 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.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/cobra v1.10.2 // 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/thomaspoignant/go-feature-flag v1.42.0 // indirect github.com/tjhop/slog-gokit v0.1.5 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // 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.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.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/instrumentation/net/http/otelhttp v0.64.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/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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.45.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect - k8s.io/kms v0.34.2 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/component-base v0.34.3 // indirect + k8s.io/kms v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 1d7387b28b3..1c9800a8bab 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -1,12 +1,16 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +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/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= @@ -38,14 +42,20 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B 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/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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 v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 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/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 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/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= @@ -60,15 +70,19 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM= +github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w= 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/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= @@ -106,14 +120,40 @@ 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -127,7 +167,6 @@ 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= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -157,8 +196,8 @@ 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -174,8 +213,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.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-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= 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-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -184,16 +223,18 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= +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.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= @@ -286,8 +327,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -341,6 +382,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 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/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= +github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= 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= @@ -355,6 +398,12 @@ 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/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/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -388,8 +437,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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= @@ -398,8 +447,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -417,8 +466,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 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= @@ -440,6 +489,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +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/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= @@ -447,8 +500,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= @@ -461,12 +514,12 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 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.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.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= -go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= -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/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= +go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= 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= @@ -475,40 +528,46 @@ 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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= 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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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.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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= @@ -521,15 +580,15 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -542,12 +601,12 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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= @@ -555,8 +614,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ 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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -578,17 +637,17 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -596,8 +655,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -611,10 +670,10 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= @@ -625,15 +684,15 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= 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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -651,30 +710,30 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= -k8s.io/kms v0.34.2/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= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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= +k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE= +k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index f9a08562cb0..f624dc117bc 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -16,6 +16,6 @@ v0alpha1Version: { } kinds: [ pluginV0Alpha1, - pluginMetaV0Alpha1, + metaV0Alpha1, ] } diff --git a/apps/plugins/kinds/pluginmeta.cue b/apps/plugins/kinds/meta.cue similarity index 88% rename from apps/plugins/kinds/pluginmeta.cue rename to apps/plugins/kinds/meta.cue index 03b1ff10ff2..d29308e2ca2 100644 --- a/apps/plugins/kinds/pluginmeta.cue +++ b/apps/plugins/kinds/meta.cue @@ -1,12 +1,29 @@ package plugins -pluginMetaV0Alpha1: { - kind: "PluginMeta" - plural: "pluginsmeta" +metaV0Alpha1: { + kind: "Meta" scope: "Namespaced" schema: { spec: { - pluginJSON: #JSONData, + pluginJson: #JSONData + class: "core" | "external" + module?: { + path: string + hash?: string + loadingStrategy?: "fetch" | "script" + } + baseURL?: string + signature?: { + status: "internal" | "valid" | "invalid" | "modified" | "unsigned" + type?: "grafana" | "commercial" | "community" | "private" | "private-glob" + org?: string + } + angular?: { + detected: bool + } + translations?: [string]: string + // +listType=atomic + children?: [...string] } } } diff --git a/apps/plugins/kinds/plugin.cue b/apps/plugins/kinds/plugin.cue index dc16a3f6de4..854448a5b72 100644 --- a/apps/plugins/kinds/plugin.cue +++ b/apps/plugins/kinds/plugin.cue @@ -9,7 +9,6 @@ pluginV0Alpha1: { id: string version: string url?: string - class: "core" | "external" } } } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_client_gen.go similarity index 50% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_client_gen.go index e7788e27a33..4acc2b635e8 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_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 MetaClient struct { + client *resource.TypedClient[*Meta, *MetaList] } -func NewPluginMetaClient(client resource.Client) *PluginMetaClient { - return &PluginMetaClient{ - client: resource.NewTypedClient[*PluginMeta, *PluginMetaList](client, PluginMetaKind()), +func NewMetaClient(client resource.Client) *MetaClient { + return &MetaClient{ + client: resource.NewTypedClient[*Meta, *MetaList](client, MetaKind()), } } -func NewPluginMetaClientFromGenerator(generator resource.ClientGenerator) (*PluginMetaClient, error) { - c, err := generator.ClientFor(PluginMetaKind()) +func NewMetaClientFromGenerator(generator resource.ClientGenerator) (*MetaClient, error) { + c, err := generator.ClientFor(MetaKind()) if err != nil { return nil, err } - return NewPluginMetaClient(c), nil + return NewMetaClient(c), nil } -func (c *PluginMetaClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginMeta, error) { +func (c *MetaClient) Get(ctx context.Context, identifier resource.Identifier) (*Meta, error) { return c.client.Get(ctx, identifier) } -func (c *PluginMetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *MetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*MetaList, error) { return c.client.List(ctx, namespace, opts) } -func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *MetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*MetaList, 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 *MetaClient) Create(ctx context.Context, obj *Meta, opts resource.CreateOptions) (*Meta, error) { // Make sure apiVersion and kind are set obj.APIVersion = GroupVersion.Identifier() - obj.Kind = PluginMetaKind().Kind() + obj.Kind = MetaKind().Kind() return c.client.Create(ctx, obj, opts) } -func (c *PluginMetaClient) Update(ctx context.Context, obj *PluginMeta, opts resource.UpdateOptions) (*PluginMeta, error) { +func (c *MetaClient) Update(ctx context.Context, obj *Meta, opts resource.UpdateOptions) (*Meta, 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 *MetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Meta, 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 *MetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus MetaStatus, opts resource.UpdateOptions) (*Meta, error) { + return c.client.Update(ctx, &Meta{ TypeMeta: metav1.TypeMeta{ - Kind: PluginMetaKind().Kind(), + Kind: MetaKind().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 *MetaClient) 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/plugins/pkg/apis/plugins/v0alpha1/meta_codec_gen.go similarity index 56% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_codec_gen.go index 77fb6f918a2..c152eb63e79 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_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{} +// MetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type MetaJSONCodec struct{} // Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` -func (*PluginMetaJSONCodec) Read(reader io.Reader, into resource.Object) error { +func (*MetaJSONCodec) 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 (*MetaJSONCodec) Write(writer io.Writer, from resource.Object) error { return json.NewEncoder(writer).Encode(from) } // Interface compliance checks -var _ resource.Codec = &PluginMetaJSONCodec{} +var _ resource.Codec = &MetaJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_metadata_gen.go similarity index 85% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_metadata_gen.go index 7d3b3c9c6b8..9de0658352f 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_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 MetaMetadata 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{ +// NewMetaMetadata creates a new MetaMetadata object. +func NewMetaMetadata() *MetaMetadata { + return &MetaMetadata{ 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/meta_object_gen.go similarity index 68% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_object_gen.go index dac431ebf12..e42a1ad5d80 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_object_gen.go @@ -15,22 +15,29 @@ import ( ) // +k8s:openapi-gen=true -type PluginMeta struct { +type Meta 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 Meta + Spec MetaSpec `json:"spec" yaml:"spec"` - Status PluginMetaStatus `json:"status" yaml:"status"` + Status MetaStatus `json:"status" yaml:"status"` } -func (o *PluginMeta) GetSpec() any { +func NewMeta() *Meta { + return &Meta{ + Spec: *NewMetaSpec(), + Status: *NewMetaStatus(), + } +} + +func (o *Meta) GetSpec() any { return o.Spec } -func (o *PluginMeta) SetSpec(spec any) error { - cast, ok := spec.(PluginMetaSpec) +func (o *Meta) SetSpec(spec any) error { + cast, ok := spec.(MetaSpec) if !ok { return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) } @@ -38,13 +45,13 @@ func (o *PluginMeta) SetSpec(spec any) error { return nil } -func (o *PluginMeta) GetSubresources() map[string]any { +func (o *Meta) GetSubresources() map[string]any { return map[string]any{ "status": o.Status, } } -func (o *PluginMeta) GetSubresource(name string) (any, bool) { +func (o *Meta) GetSubresource(name string) (any, bool) { switch name { case "status": return o.Status, true @@ -53,12 +60,12 @@ func (o *PluginMeta) GetSubresource(name string) (any, bool) { } } -func (o *PluginMeta) SetSubresource(name string, value any) error { +func (o *Meta) SetSubresource(name string, value any) error { switch name { case "status": - cast, ok := value.(PluginMetaStatus) + cast, ok := value.(MetaStatus) 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 MetaStatus", value) } o.Status = cast return nil @@ -67,7 +74,7 @@ func (o *PluginMeta) SetSubresource(name string, value any) error { } } -func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { +func (o *Meta) GetStaticMetadata() resource.StaticMetadata { gvk := o.GroupVersionKind() return resource.StaticMetadata{ Name: o.ObjectMeta.Name, @@ -78,7 +85,7 @@ func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { } } -func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { +func (o *Meta) SetStaticMetadata(metadata resource.StaticMetadata) { o.Name = metadata.Name o.Namespace = metadata.Namespace o.SetGroupVersionKind(schema.GroupVersionKind{ @@ -88,7 +95,7 @@ func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { }) } -func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { +func (o *Meta) GetCommonMetadata() resource.CommonMetadata { dt := o.DeletionTimestamp var deletionTimestamp *time.Time if dt != nil { @@ -120,7 +127,7 @@ func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { } } -func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { +func (o *Meta) SetCommonMetadata(metadata resource.CommonMetadata) { o.UID = types.UID(metadata.UID) o.ResourceVersion = metadata.ResourceVersion o.Generation = metadata.Generation @@ -165,7 +172,7 @@ func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { } } -func (o *PluginMeta) GetCreatedBy() string { +func (o *Meta) GetCreatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -173,7 +180,7 @@ func (o *PluginMeta) GetCreatedBy() string { return o.ObjectMeta.Annotations["grafana.com/createdBy"] } -func (o *PluginMeta) SetCreatedBy(createdBy string) { +func (o *Meta) SetCreatedBy(createdBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -181,7 +188,7 @@ func (o *PluginMeta) SetCreatedBy(createdBy string) { o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy } -func (o *PluginMeta) GetUpdateTimestamp() time.Time { +func (o *Meta) GetUpdateTimestamp() time.Time { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -190,7 +197,7 @@ func (o *PluginMeta) GetUpdateTimestamp() time.Time { return parsed } -func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { +func (o *Meta) SetUpdateTimestamp(updateTimestamp time.Time) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -198,7 +205,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 *Meta) GetUpdatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -206,7 +213,7 @@ func (o *PluginMeta) GetUpdatedBy() string { return o.ObjectMeta.Annotations["grafana.com/updatedBy"] } -func (o *PluginMeta) SetUpdatedBy(updatedBy string) { +func (o *Meta) SetUpdatedBy(updatedBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -214,21 +221,21 @@ func (o *PluginMeta) SetUpdatedBy(updatedBy string) { o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy } -func (o *PluginMeta) Copy() resource.Object { +func (o *Meta) Copy() resource.Object { return resource.CopyObject(o) } -func (o *PluginMeta) DeepCopyObject() runtime.Object { +func (o *Meta) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMeta) DeepCopy() *PluginMeta { - cpy := &PluginMeta{} +func (o *Meta) DeepCopy() *Meta { + cpy := &Meta{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { +func (o *Meta) DeepCopyInto(dst *Meta) { dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) @@ -237,34 +244,34 @@ func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { } // Interface compliance compile-time check -var _ resource.Object = &PluginMeta{} +var _ resource.Object = &Meta{} // +k8s:openapi-gen=true -type PluginMetaList struct { +type MetaList struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ListMeta `json:"metadata" yaml:"metadata"` - Items []PluginMeta `json:"items" yaml:"items"` + Items []Meta `json:"items" yaml:"items"` } -func (o *PluginMetaList) DeepCopyObject() runtime.Object { +func (o *MetaList) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMetaList) Copy() resource.ListObject { - cpy := &PluginMetaList{ +func (o *MetaList) Copy() resource.ListObject { + cpy := &MetaList{ TypeMeta: o.TypeMeta, - Items: make([]PluginMeta, len(o.Items)), + Items: make([]Meta, 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().(*Meta); ok { cpy.Items[i] = *item } } return cpy } -func (o *PluginMetaList) GetItems() []resource.Object { +func (o *MetaList) 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 +279,48 @@ func (o *PluginMetaList) GetItems() []resource.Object { return items } -func (o *PluginMetaList) SetItems(items []resource.Object) { - o.Items = make([]PluginMeta, len(items)) +func (o *MetaList) SetItems(items []resource.Object) { + o.Items = make([]Meta, len(items)) for i := 0; i < len(items); i++ { - o.Items[i] = *items[i].(*PluginMeta) + o.Items[i] = *items[i].(*Meta) } } -func (o *PluginMetaList) DeepCopy() *PluginMetaList { - cpy := &PluginMetaList{} +func (o *MetaList) DeepCopy() *MetaList { + cpy := &MetaList{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMetaList) DeepCopyInto(dst *PluginMetaList) { +func (o *MetaList) DeepCopyInto(dst *MetaList) { resource.CopyObjectInto(dst, o) } // Interface compliance compile-time check -var _ resource.ListObject = &PluginMetaList{} +var _ resource.ListObject = &MetaList{} // Copy methods for all subresource types // DeepCopy creates a full deep copy of Spec -func (s *PluginMetaSpec) DeepCopy() *PluginMetaSpec { - cpy := &PluginMetaSpec{} +func (s *MetaSpec) DeepCopy() *MetaSpec { + cpy := &MetaSpec{} s.DeepCopyInto(cpy) return cpy } // DeepCopyInto deep copies Spec into another Spec object -func (s *PluginMetaSpec) DeepCopyInto(dst *PluginMetaSpec) { +func (s *MetaSpec) DeepCopyInto(dst *MetaSpec) { 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 MetaStatus +func (s *MetaStatus) DeepCopy() *MetaStatus { + cpy := &MetaStatus{} s.DeepCopyInto(cpy) return cpy } -// DeepCopyInto deep copies PluginMetaStatus into another PluginMetaStatus object -func (s *PluginMetaStatus) DeepCopyInto(dst *PluginMetaStatus) { +// DeepCopyInto deep copies MetaStatus into another MetaStatus object +func (s *MetaStatus) DeepCopyInto(dst *MetaStatus) { resource.CopyObjectInto(dst, s) } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_schema_gen.go new file mode 100644 index 00000000000..38b45140ff6 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_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 ( + schemaMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", NewMeta(), &MetaList{}, resource.WithKind("Meta"), + resource.WithPlural("metas"), resource.WithScope(resource.NamespacedScope)) + kindMeta = resource.Kind{ + Schema: schemaMeta, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &MetaJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func MetaKind() resource.Kind { + return kindMeta +} + +// Schema returns a resource.SimpleSchema representation of Meta +func MetaSchema() *resource.SimpleSchema { + return schemaMeta +} + +// Interface compliance checks +var _ resource.Schema = kindMeta diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go new file mode 100644 index 00000000000..f3671a040db --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go @@ -0,0 +1,554 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// 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 MetaJSONData struct { + // Unique name of the plugin + Id string `json:"id"` + // Plugin type + Type MetaJSONDataType `json:"type"` + // Human-readable name of the plugin + Name string `json:"name"` + // Metadata for the plugin + Info MetaInfo `json:"info"` + // Dependency information + Dependencies MetaDependencies `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 *MetaJSONDataCategory `json:"category,omitempty"` + EnterpriseFeatures *MetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"` + Executable *string `json:"executable,omitempty"` + HideFromList *bool `json:"hideFromList,omitempty"` + // +listType=atomic + Includes []MetaInclude `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 *MetaQueryOptions `json:"queryOptions,omitempty"` + // +listType=atomic + Routes []MetaRoute `json:"routes,omitempty"` + SkipDataQuery *bool `json:"skipDataQuery,omitempty"` + State *MetaJSONDataState `json:"state,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + Suggestions *bool `json:"suggestions,omitempty"` + Tracing *bool `json:"tracing,omitempty"` + Iam *MetaIAM `json:"iam,omitempty"` + // +listType=atomic + Roles []MetaRole `json:"roles,omitempty"` + Extensions *MetaExtensions `json:"extensions,omitempty"` +} + +// NewMetaJSONData creates a new MetaJSONData object. +func NewMetaJSONData() *MetaJSONData { + return &MetaJSONData{ + Info: *NewMetaInfo(), + Dependencies: *NewMetaDependencies(), + } +} + +// +k8s:openapi-gen=true +type MetaInfo struct { + // Required fields + // +listType=set + Keywords []string `json:"keywords"` + Logos MetaV0alpha1InfoLogos `json:"logos"` + Updated string `json:"updated"` + Version string `json:"version"` + // Optional fields + Author *MetaV0alpha1InfoAuthor `json:"author,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Links []MetaV0alpha1InfoLinks `json:"links,omitempty"` + // +listType=atomic + Screenshots []MetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"` +} + +// NewMetaInfo creates a new MetaInfo object. +func NewMetaInfo() *MetaInfo { + return &MetaInfo{ + Keywords: []string{}, + Logos: *NewMetaV0alpha1InfoLogos(), + } +} + +// +k8s:openapi-gen=true +type MetaDependencies struct { + // Required field + GrafanaDependency string `json:"grafanaDependency"` + // Optional fields + GrafanaVersion *string `json:"grafanaVersion,omitempty"` + // +listType=set + // +listMapKey=id + Plugins []MetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"` + Extensions *MetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"` +} + +// NewMetaDependencies creates a new MetaDependencies object. +func NewMetaDependencies() *MetaDependencies { + return &MetaDependencies{} +} + +// +k8s:openapi-gen=true +type MetaEnterpriseFeatures struct { + // Allow additional properties + HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` +} + +// NewMetaEnterpriseFeatures creates a new MetaEnterpriseFeatures object. +func NewMetaEnterpriseFeatures() *MetaEnterpriseFeatures { + return &MetaEnterpriseFeatures{ + HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), + } +} + +// +k8s:openapi-gen=true +type MetaInclude struct { + Uid *string `json:"uid,omitempty"` + Type *MetaIncludeType `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + Component *string `json:"component,omitempty"` + Role *MetaIncludeRole `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"` +} + +// NewMetaInclude creates a new MetaInclude object. +func NewMetaInclude() *MetaInclude { + return &MetaInclude{} +} + +// +k8s:openapi-gen=true +type MetaQueryOptions struct { + MaxDataPoints *bool `json:"maxDataPoints,omitempty"` + MinInterval *bool `json:"minInterval,omitempty"` + CacheTimeout *bool `json:"cacheTimeout,omitempty"` +} + +// NewMetaQueryOptions creates a new MetaQueryOptions object. +func NewMetaQueryOptions() *MetaQueryOptions { + return &MetaQueryOptions{} +} + +// +k8s:openapi-gen=true +type MetaRoute 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 *MetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` + JwtTokenAuth *MetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` + // +listType=atomic + UrlParams []MetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"` +} + +// NewMetaRoute creates a new MetaRoute object. +func NewMetaRoute() *MetaRoute { + return &MetaRoute{} +} + +// +k8s:openapi-gen=true +type MetaIAM struct { + // +listType=atomic + Permissions []MetaV0alpha1IAMPermissions `json:"permissions,omitempty"` +} + +// NewMetaIAM creates a new MetaIAM object. +func NewMetaIAM() *MetaIAM { + return &MetaIAM{} +} + +// +k8s:openapi-gen=true +type MetaRole struct { + Role *MetaV0alpha1RoleRole `json:"role,omitempty"` + // +listType=set + Grants []string `json:"grants,omitempty"` +} + +// NewMetaRole creates a new MetaRole object. +func NewMetaRole() *MetaRole { + return &MetaRole{} +} + +// +k8s:openapi-gen=true +type MetaExtensions struct { + // +listType=atomic + AddedComponents []MetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` + // +listType=atomic + AddedLinks []MetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` + // +listType=set + // +listMapKey=id + ExposedComponents []MetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` + // +listType=set + // +listMapKey=id + ExtensionPoints []MetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` +} + +// NewMetaExtensions creates a new MetaExtensions object. +func NewMetaExtensions() *MetaExtensions { + return &MetaExtensions{} +} + +// +k8s:openapi-gen=true +type MetaSpec struct { + PluginJson MetaJSONData `json:"pluginJson"` + Class MetaSpecClass `json:"class"` + Module *MetaV0alpha1SpecModule `json:"module,omitempty"` + BaseURL *string `json:"baseURL,omitempty"` + Signature *MetaV0alpha1SpecSignature `json:"signature,omitempty"` + Angular *MetaV0alpha1SpecAngular `json:"angular,omitempty"` + Translations map[string]string `json:"translations,omitempty"` + // +listType=atomic + Children []string `json:"children,omitempty"` +} + +// NewMetaSpec creates a new MetaSpec object. +func NewMetaSpec() *MetaSpec { + return &MetaSpec{ + PluginJson: *NewMetaJSONData(), + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoLogos struct { + Small string `json:"small"` + Large string `json:"large"` +} + +// NewMetaV0alpha1InfoLogos creates a new MetaV0alpha1InfoLogos object. +func NewMetaV0alpha1InfoLogos() *MetaV0alpha1InfoLogos { + return &MetaV0alpha1InfoLogos{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoAuthor struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewMetaV0alpha1InfoAuthor creates a new MetaV0alpha1InfoAuthor object. +func NewMetaV0alpha1InfoAuthor() *MetaV0alpha1InfoAuthor { + return &MetaV0alpha1InfoAuthor{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoLinks struct { + Name *string `json:"name,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewMetaV0alpha1InfoLinks creates a new MetaV0alpha1InfoLinks object. +func NewMetaV0alpha1InfoLinks() *MetaV0alpha1InfoLinks { + return &MetaV0alpha1InfoLinks{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoScreenshots struct { + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +// NewMetaV0alpha1InfoScreenshots creates a new MetaV0alpha1InfoScreenshots object. +func NewMetaV0alpha1InfoScreenshots() *MetaV0alpha1InfoScreenshots { + return &MetaV0alpha1InfoScreenshots{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesPlugins struct { + Id string `json:"id"` + Type MetaV0alpha1DependenciesPluginsType `json:"type"` + Name string `json:"name"` +} + +// NewMetaV0alpha1DependenciesPlugins creates a new MetaV0alpha1DependenciesPlugins object. +func NewMetaV0alpha1DependenciesPlugins() *MetaV0alpha1DependenciesPlugins { + return &MetaV0alpha1DependenciesPlugins{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesExtensions struct { + // +listType=set + ExposedComponents []string `json:"exposedComponents,omitempty"` +} + +// NewMetaV0alpha1DependenciesExtensions creates a new MetaV0alpha1DependenciesExtensions object. +func NewMetaV0alpha1DependenciesExtensions() *MetaV0alpha1DependenciesExtensions { + return &MetaV0alpha1DependenciesExtensions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewMetaV0alpha1RouteTokenAuth creates a new MetaV0alpha1RouteTokenAuth object. +func NewMetaV0alpha1RouteTokenAuth() *MetaV0alpha1RouteTokenAuth { + return &MetaV0alpha1RouteTokenAuth{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteJwtTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewMetaV0alpha1RouteJwtTokenAuth creates a new MetaV0alpha1RouteJwtTokenAuth object. +func NewMetaV0alpha1RouteJwtTokenAuth() *MetaV0alpha1RouteJwtTokenAuth { + return &MetaV0alpha1RouteJwtTokenAuth{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteUrlParams struct { + Name *string `json:"name,omitempty"` + Content *string `json:"content,omitempty"` +} + +// NewMetaV0alpha1RouteUrlParams creates a new MetaV0alpha1RouteUrlParams object. +func NewMetaV0alpha1RouteUrlParams() *MetaV0alpha1RouteUrlParams { + return &MetaV0alpha1RouteUrlParams{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1IAMPermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewMetaV0alpha1IAMPermissions creates a new MetaV0alpha1IAMPermissions object. +func NewMetaV0alpha1IAMPermissions() *MetaV0alpha1IAMPermissions { + return &MetaV0alpha1IAMPermissions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RoleRolePermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewMetaV0alpha1RoleRolePermissions creates a new MetaV0alpha1RoleRolePermissions object. +func NewMetaV0alpha1RoleRolePermissions() *MetaV0alpha1RoleRolePermissions { + return &MetaV0alpha1RoleRolePermissions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RoleRole struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Permissions []MetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"` +} + +// NewMetaV0alpha1RoleRole creates a new MetaV0alpha1RoleRole object. +func NewMetaV0alpha1RoleRole() *MetaV0alpha1RoleRole { + return &MetaV0alpha1RoleRole{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsAddedComponents struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsAddedComponents creates a new MetaV0alpha1ExtensionsAddedComponents object. +func NewMetaV0alpha1ExtensionsAddedComponents() *MetaV0alpha1ExtensionsAddedComponents { + return &MetaV0alpha1ExtensionsAddedComponents{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsAddedLinks struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsAddedLinks creates a new MetaV0alpha1ExtensionsAddedLinks object. +func NewMetaV0alpha1ExtensionsAddedLinks() *MetaV0alpha1ExtensionsAddedLinks { + return &MetaV0alpha1ExtensionsAddedLinks{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsExposedComponents struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsExposedComponents creates a new MetaV0alpha1ExtensionsExposedComponents object. +func NewMetaV0alpha1ExtensionsExposedComponents() *MetaV0alpha1ExtensionsExposedComponents { + return &MetaV0alpha1ExtensionsExposedComponents{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsExtensionPoints struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsExtensionPoints creates a new MetaV0alpha1ExtensionsExtensionPoints object. +func NewMetaV0alpha1ExtensionsExtensionPoints() *MetaV0alpha1ExtensionsExtensionPoints { + return &MetaV0alpha1ExtensionsExtensionPoints{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecModule struct { + Path string `json:"path"` + Hash *string `json:"hash,omitempty"` + LoadingStrategy *MetaV0alpha1SpecModuleLoadingStrategy `json:"loadingStrategy,omitempty"` +} + +// NewMetaV0alpha1SpecModule creates a new MetaV0alpha1SpecModule object. +func NewMetaV0alpha1SpecModule() *MetaV0alpha1SpecModule { + return &MetaV0alpha1SpecModule{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignature struct { + Status MetaV0alpha1SpecSignatureStatus `json:"status"` + Type *MetaV0alpha1SpecSignatureType `json:"type,omitempty"` + Org *string `json:"org,omitempty"` +} + +// NewMetaV0alpha1SpecSignature creates a new MetaV0alpha1SpecSignature object. +func NewMetaV0alpha1SpecSignature() *MetaV0alpha1SpecSignature { + return &MetaV0alpha1SpecSignature{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecAngular struct { + Detected bool `json:"detected"` +} + +// NewMetaV0alpha1SpecAngular creates a new MetaV0alpha1SpecAngular object. +func NewMetaV0alpha1SpecAngular() *MetaV0alpha1SpecAngular { + return &MetaV0alpha1SpecAngular{} +} + +// +k8s:openapi-gen=true +type MetaJSONDataType string + +const ( + MetaJSONDataTypeApp MetaJSONDataType = "app" + MetaJSONDataTypeDatasource MetaJSONDataType = "datasource" + MetaJSONDataTypePanel MetaJSONDataType = "panel" + MetaJSONDataTypeRenderer MetaJSONDataType = "renderer" +) + +// +k8s:openapi-gen=true +type MetaJSONDataCategory string + +const ( + MetaJSONDataCategoryTsdb MetaJSONDataCategory = "tsdb" + MetaJSONDataCategoryLogging MetaJSONDataCategory = "logging" + MetaJSONDataCategoryCloud MetaJSONDataCategory = "cloud" + MetaJSONDataCategoryTracing MetaJSONDataCategory = "tracing" + MetaJSONDataCategoryProfiling MetaJSONDataCategory = "profiling" + MetaJSONDataCategorySql MetaJSONDataCategory = "sql" + MetaJSONDataCategoryEnterprise MetaJSONDataCategory = "enterprise" + MetaJSONDataCategoryIot MetaJSONDataCategory = "iot" + MetaJSONDataCategoryOther MetaJSONDataCategory = "other" +) + +// +k8s:openapi-gen=true +type MetaJSONDataState string + +const ( + MetaJSONDataStateAlpha MetaJSONDataState = "alpha" + MetaJSONDataStateBeta MetaJSONDataState = "beta" +) + +// +k8s:openapi-gen=true +type MetaIncludeType string + +const ( + MetaIncludeTypeDashboard MetaIncludeType = "dashboard" + MetaIncludeTypePage MetaIncludeType = "page" + MetaIncludeTypePanel MetaIncludeType = "panel" + MetaIncludeTypeDatasource MetaIncludeType = "datasource" +) + +// +k8s:openapi-gen=true +type MetaIncludeRole string + +const ( + MetaIncludeRoleAdmin MetaIncludeRole = "Admin" + MetaIncludeRoleEditor MetaIncludeRole = "Editor" + MetaIncludeRoleViewer MetaIncludeRole = "Viewer" +) + +// +k8s:openapi-gen=true +type MetaSpecClass string + +const ( + MetaSpecClassCore MetaSpecClass = "core" + MetaSpecClassExternal MetaSpecClass = "external" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesPluginsType string + +const ( + MetaV0alpha1DependenciesPluginsTypeApp MetaV0alpha1DependenciesPluginsType = "app" + MetaV0alpha1DependenciesPluginsTypeDatasource MetaV0alpha1DependenciesPluginsType = "datasource" + MetaV0alpha1DependenciesPluginsTypePanel MetaV0alpha1DependenciesPluginsType = "panel" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecModuleLoadingStrategy string + +const ( + MetaV0alpha1SpecModuleLoadingStrategyFetch MetaV0alpha1SpecModuleLoadingStrategy = "fetch" + MetaV0alpha1SpecModuleLoadingStrategyScript MetaV0alpha1SpecModuleLoadingStrategy = "script" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignatureStatus string + +const ( + MetaV0alpha1SpecSignatureStatusInternal MetaV0alpha1SpecSignatureStatus = "internal" + MetaV0alpha1SpecSignatureStatusValid MetaV0alpha1SpecSignatureStatus = "valid" + MetaV0alpha1SpecSignatureStatusInvalid MetaV0alpha1SpecSignatureStatus = "invalid" + MetaV0alpha1SpecSignatureStatusModified MetaV0alpha1SpecSignatureStatus = "modified" + MetaV0alpha1SpecSignatureStatusUnsigned MetaV0alpha1SpecSignatureStatus = "unsigned" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignatureType string + +const ( + MetaV0alpha1SpecSignatureTypeGrafana MetaV0alpha1SpecSignatureType = "grafana" + MetaV0alpha1SpecSignatureTypeCommercial MetaV0alpha1SpecSignatureType = "commercial" + MetaV0alpha1SpecSignatureTypeCommunity MetaV0alpha1SpecSignatureType = "community" + MetaV0alpha1SpecSignatureTypePrivate MetaV0alpha1SpecSignatureType = "private" + MetaV0alpha1SpecSignatureTypePrivateGlob MetaV0alpha1SpecSignatureType = "private-glob" +) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go similarity index 52% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go index 60fa37dbb32..5f37ac58fb7 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go @@ -3,42 +3,42 @@ package v0alpha1 // +k8s:openapi-gen=true -type PluginMetastatusOperatorState struct { +type MetastatusOperatorState 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 MetaStatusOperatorStateState `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{} +// NewMetastatusOperatorState creates a new MetastatusOperatorState object. +func NewMetastatusOperatorState() *MetastatusOperatorState { + return &MetastatusOperatorState{} } // +k8s:openapi-gen=true -type PluginMetaStatus struct { +type MetaStatus 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]MetastatusOperatorState `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{} +// NewMetaStatus creates a new MetaStatus object. +func NewMetaStatus() *MetaStatus { + return &MetaStatus{} } // +k8s:openapi-gen=true -type PluginMetaStatusOperatorStateState string +type MetaStatusOperatorStateState string const ( - PluginMetaStatusOperatorStateStateSuccess PluginMetaStatusOperatorStateState = "success" - PluginMetaStatusOperatorStateStateInProgress PluginMetaStatusOperatorStateState = "in_progress" - PluginMetaStatusOperatorStateStateFailed PluginMetaStatusOperatorStateState = "failed" + MetaStatusOperatorStateStateSuccess MetaStatusOperatorStateState = "success" + MetaStatusOperatorStateStateInProgress MetaStatusOperatorStateState = "in_progress" + MetaStatusOperatorStateStateFailed MetaStatusOperatorStateState = "failed" ) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go index d2cdedba399..b92b11f4cba 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go @@ -25,6 +25,13 @@ type Plugin struct { Status PluginStatus `json:"status" yaml:"status"` } +func NewPlugin() *Plugin { + return &Plugin{ + Spec: *NewPluginSpec(), + Status: *NewPluginStatus(), + } +} + func (o *Plugin) GetSpec() any { return o.Spec } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go index 97024275a1c..144ddd5af89 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaPlugin = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &Plugin{}, &PluginList{}, resource.WithKind("Plugin"), + schemaPlugin = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", NewPlugin(), &PluginList{}, resource.WithKind("Plugin"), resource.WithPlural("plugins"), resource.WithScope(resource.NamespacedScope)) kindPlugin = resource.Kind{ Schema: schemaPlugin, diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go index 9ae14bcbdc5..51f5440eb92 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go @@ -4,21 +4,12 @@ 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"` + Id string `json:"id"` + Version string `json:"version"` + Url *string `json:"url,omitempty"` } // 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" -) 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 14a3b0515bd..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go +++ /dev/null @@ -1,474 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// 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"` - Suggestions *bool `json:"suggestions,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 string `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 e09c70244a7..7b87dcf0ea7 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,12 +20,12 @@ import ( ) var ( - 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"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"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) - 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":{"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"},"suggestions":{"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) + 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"]},"spec":{"additionalProperties":false,"properties":{"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version"],"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) + rawSchemaMetav0alpha1 = []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":{"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"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"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"},"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":{"angular":{"additionalProperties":false,"properties":{"detected":{"type":"boolean"}},"required":["detected"],"type":"object"},"baseURL":{"type":"string"},"children":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"class":{"enum":["core","external"],"type":"string"},"module":{"additionalProperties":false,"properties":{"hash":{"type":"string"},"loadingStrategy":{"enum":["fetch","script"],"type":"string"},"path":{"type":"string"}},"required":["path"],"type":"object"},"pluginJson":{"$ref":"#/components/schemas/JSONData"},"signature":{"additionalProperties":false,"properties":{"org":{"type":"string"},"status":{"enum":["internal","valid","invalid","modified","unsigned"],"type":"string"},"type":{"enum":["grafana","commercial","community","private","private-glob"],"type":"string"}},"required":["status"],"type":"object"},"translations":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["pluginJson","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"}}`) + versionSchemaMetav0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaMetav0alpha1, &versionSchemaMetav0alpha1) ) var appManifestData = app.ManifestData{ @@ -46,11 +46,11 @@ var appManifestData = app.ManifestData{ }, { - Kind: "PluginMeta", - Plural: "PluginMetas", + Kind: "Meta", + Plural: "Metas", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaPluginMetav0alpha1, + Schema: &versionSchemaMetav0alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -71,8 +71,8 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "Plugin/v0alpha1": v0alpha1.PluginKind(), - "PluginMeta/v0alpha1": v0alpha1.PluginMetaKind(), + "Plugin/v0alpha1": v0alpha1.PluginKind(), + "Meta/v0alpha1": v0alpha1.MetaKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 3d17cc34221..c296bccd9e1 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "sync" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" @@ -16,7 +17,6 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/klog/v2" - authlib "github.com/grafana/authlib/types" pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis" pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" @@ -43,7 +43,7 @@ func New(cfg app.Config) (app.App, error) { Kind: pluginsv0alpha1.PluginKind(), }, { - Kind: pluginsv0alpha1.PluginMetaKind(), + Kind: pluginsv0alpha1.MetaKind(), }, }, } @@ -69,6 +69,7 @@ type PluginAppConfig struct { } func ProvideAppInstaller( + authorizer authorizer.Authorizer, metaProviderManager *meta.ProviderManager, ) (*PluginAppInstaller, error) { specificConfig := &PluginAppConfig{ @@ -87,31 +88,30 @@ func ProvideAppInstaller( appInstaller := &PluginAppInstaller{ AppInstaller: defaultInstaller, + authorizer: authorizer, metaManager: metaProviderManager, ready: make(chan struct{}), } return appInstaller, nil } -func (p *PluginAppInstaller) WithAccessChecker(access authlib.AccessChecker) *PluginAppInstaller { - p.access = access - return p -} - type PluginAppInstaller struct { appsdkapiserver.AppInstaller metaManager *meta.ProviderManager - access authlib.AccessChecker + authorizer authorizer.Authorizer // restConfig is set during InitializeApp and used by the client factory restConfig *restclient.Config ready chan struct{} + readyOnce sync.Once } func (p *PluginAppInstaller) InitializeApp(restConfig restclient.Config) error { if p.restConfig == nil { p.restConfig = &restConfig - close(p.ready) + p.readyOnce.Do(func() { + close(p.ready) + }) } return p.AppInstaller.InitializeApp(restConfig) } @@ -137,9 +137,9 @@ func (p *PluginAppInstaller) InstallAPIs( return client, nil } - pluginMetaGVR := pluginsv0alpha1.PluginMetaKind().GroupVersionResource() + pluginMetaGVR := pluginsv0alpha1.MetaKind().GroupVersionResource() replacedStorage := map[schema.GroupVersionResource]rest.Storage{ - pluginMetaGVR: NewPluginMetaStorage(p.metaManager, clientFactory), + pluginMetaGVR: NewMetaStorage(p.metaManager, clientFactory), } wrappedServer := &customStorageWrapper{ wrapped: server, @@ -149,34 +149,5 @@ func (p *PluginAppInstaller) InstallAPIs( } func (p *PluginAppInstaller) GetAuthorizer() authorizer.Authorizer { - if p.access == nil { - return nil - } - - return authorizer.AuthorizerFunc( - func(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { - info, ok := authlib.AuthInfoFrom(ctx) - if !ok { - return authorizer.DecisionDeny, "failed to get auth info", nil - } - - res, err := p.access.Check(ctx, info, authlib.CheckRequest{ - Verb: a.GetVerb(), - Group: a.GetAPIGroup(), - Resource: a.GetResource(), - Name: a.GetName(), - Namespace: a.GetNamespace(), - Subresource: a.GetSubresource(), - Path: a.GetPath(), - }, "") - if err != nil { - return authorizer.DecisionDeny, "failed to perform authorization", err - } - - if !res.Allowed { - return authorizer.DecisionDeny, "permission denied", nil - } - - return authorizer.DecisionAllow, "", nil - }) + return p.authorizer } diff --git a/apps/plugins/pkg/app/authorizer.go b/apps/plugins/pkg/app/authorizer.go deleted file mode 100644 index b6a800694f7..00000000000 --- a/apps/plugins/pkg/app/authorizer.go +++ /dev/null @@ -1,32 +0,0 @@ -package app - -import ( - "context" - - "k8s.io/apiserver/pkg/authorization/authorizer" - - "github.com/grafana/grafana/pkg/apimachinery/identity" -) - -func GetAuthorizer() authorizer.Authorizer { - return authorizer.AuthorizerFunc(func( - ctx context.Context, attr authorizer.Attributes, - ) (authorized authorizer.Decision, reason string, err 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.HasRole(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - - return authorizer.DecisionDeny, "forbidden", nil - }) -} diff --git a/apps/plugins/pkg/app/install/registrar.go b/apps/plugins/pkg/app/install/registrar.go index 3977fd88cf0..a900db1a68d 100644 --- a/apps/plugins/pkg/app/install/registrar.go +++ b/apps/plugins/pkg/app/install/registrar.go @@ -15,16 +15,6 @@ const ( PluginInstallSourceAnnotation = "plugins.grafana.app/install-source" ) -// Class represents the plugin class type in an unversioned internal format. -// This intentionally duplicates the versioned API type (PluginInstallSpecClass) to decouple -// internal code from API version changes, making it easier to support multiple API versions. -type Class = string - -const ( - ClassCore Class = "core" - ClassExternal Class = "external" -) - type Source = string const ( @@ -36,7 +26,6 @@ type PluginInstall struct { ID string Version string URL string - Class Class Source Source } @@ -57,7 +46,6 @@ func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alph Id: p.ID, Version: p.Version, Url: url, - Class: pluginsv0alpha1.PluginSpecClass(p.Class), }, } } @@ -70,9 +58,6 @@ func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) bool { if existing.Spec.Version != update.Spec.Version { return true } - if existing.Spec.Class != update.Spec.Class { - return true // this should never really happen - } if !equalStringPointers(existing.Spec.Url, update.Spec.Url) { return true } diff --git a/apps/plugins/pkg/app/install/registrar_test.go b/apps/plugins/pkg/app/install/registrar_test.go index f1e0adbc508..6961acb98f8 100644 --- a/apps/plugins/pkg/app/install/registrar_test.go +++ b/apps/plugins/pkg/app/install/registrar_test.go @@ -26,14 +26,12 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) { 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, } @@ -54,13 +52,6 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) { }, expectUpdate: true, }, - { - name: "class differs", - modifyInstall: func(pi *PluginInstall) { - pi.Class = ClassCore - }, - expectUpdate: true, - }, { name: "url differs", modifyInstall: func(pi *PluginInstall) { @@ -109,7 +100,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1"), @@ -120,7 +110,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "2.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existing: &pluginsv0alpha1.Plugin{ @@ -135,7 +124,6 @@ func TestInstallRegistrar_Register(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, expectedUpdates: 1, @@ -145,7 +133,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existing: &pluginsv0alpha1.Plugin{ @@ -160,7 +147,6 @@ func TestInstallRegistrar_Register(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, }, @@ -169,7 +155,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-err", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existingErr: errorsK8s.NewInternalError(errors.New("boom")), @@ -410,7 +395,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { install: PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "org-1", @@ -424,7 +408,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "https://example.com/plugin.zip", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "org-1", @@ -433,25 +416,11 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { 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: "source annotation is set correctly", install: PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourceUnknown, }, namespace: "org-1", @@ -464,7 +433,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { install: PluginInstall{ ID: "my-plugin", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "my-namespace", @@ -556,7 +524,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: newURL, - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: nil, @@ -568,7 +535,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "", - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: &existingURL, @@ -580,7 +546,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "", - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: nil, @@ -592,7 +557,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: existingURL, - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: &existingURL, @@ -614,7 +578,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { Id: "plugin-1", Version: "1.0.0", Url: tt.existingURL, - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, } @@ -670,7 +633,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, setupClient: func(fc *fakePluginInstallClient) { @@ -688,7 +650,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "2.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, setupClient: func(fc *fakePluginInstallClient) { @@ -705,7 +666,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, nil } @@ -876,7 +836,6 @@ func TestInstallRegistrar_GetClientError(t *testing.T) { install := &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, } diff --git a/apps/plugins/pkg/app/meta/catalog.go b/apps/plugins/pkg/app/meta/catalog.go new file mode 100644 index 00000000000..481db9b5947 --- /dev/null +++ b/apps/plugins/pkg/app/meta/catalog.go @@ -0,0 +1,93 @@ +package meta + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "time" + + "github.com/grafana/grafana-app-sdk/logging" +) + +const ( + defaultCatalogTTL = 1 * time.Hour +) + +// CatalogProvider retrieves plugin metadata from the grafana.com API. +type CatalogProvider struct { + httpClient *http.Client + grafanaComAPIURL string + log logging.Logger + ttl time.Duration +} + +// NewCatalogProvider creates a new CatalogProvider that fetches metadata from grafana.com. +func NewCatalogProvider(grafanaComAPIURL string) *CatalogProvider { + return NewCatalogProviderWithTTL(grafanaComAPIURL, defaultCatalogTTL) +} + +// NewCatalogProviderWithTTL creates a new CatalogProvider with a custom TTL. +func NewCatalogProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CatalogProvider { + if grafanaComAPIURL == "" { + grafanaComAPIURL = "https://grafana.com/api/plugins" + } + + return &CatalogProvider{ + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + grafanaComAPIURL: grafanaComAPIURL, + log: logging.DefaultLogger, + ttl: ttl, + } +} + +// GetMeta fetches plugin metadata from grafana.com API endpoint: +// GET /api/plugins/{pluginId}/versions/{version} +func (p *CatalogProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { + u, err := url.Parse(p.grafanaComAPIURL) + if err != nil { + return nil, fmt.Errorf("invalid grafana.com API URL: %w", err) + } + + u.Path = path.Join(u.Path, pluginID, "versions", version) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "grafana-plugins-app") + + resp, err := p.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch plugin metadata: %w", err) + } + defer func() { + if err = resp.Body.Close(); err != nil { + p.log.Warn("Failed to close response body", "error", err) + } + }() + + if resp.StatusCode == http.StatusNotFound { + return nil, ErrMetaNotFound + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d from grafana.com API", resp.StatusCode) + } + + var gcomMeta grafanaComPluginVersionMeta + if err = json.NewDecoder(resp.Body).Decode(&gcomMeta); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + metaSpec := grafanaComPluginVersionMetaToMetaSpec(gcomMeta) + return &Result{ + Meta: metaSpec, + TTL: p.ttl, + }, nil +} diff --git a/apps/plugins/pkg/app/meta/cloud_test.go b/apps/plugins/pkg/app/meta/catalog_test.go similarity index 78% rename from apps/plugins/pkg/app/meta/cloud_test.go rename to apps/plugins/pkg/app/meta/catalog_test.go index bef2d1a5041..1dea816e002 100644 --- a/apps/plugins/pkg/app/meta/cloud_test.go +++ b/apps/plugins/pkg/app/meta/catalog_test.go @@ -15,14 +15,14 @@ import ( pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" ) -func TestCloudProvider_GetMeta(t *testing.T) { +func TestCatalogProvider_GetMeta(t *testing.T) { ctx := context.Background() t.Run("successfully fetches plugin metadata", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -44,13 +44,13 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, expectedMeta, result.Meta) - assert.Equal(t, defaultCloudTTL, result.TTL) + assert.Equal(t, expectedMeta, result.Meta.PluginJson) + assert.Equal(t, defaultCatalogTTL, result.TTL) }) t.Run("returns ErrMetaNotFound for 404 status", func(t *testing.T) { @@ -59,7 +59,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0") assert.Error(t, err) @@ -73,7 +73,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -89,7 +89,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -98,7 +98,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { }) t.Run("returns error for invalid API URL", func(t *testing.T) { - provider := NewCloudProvider("://invalid-url") + provider := NewCatalogProvider("://invalid-url") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -108,10 +108,10 @@ func TestCloudProvider_GetMeta(t *testing.T) { t.Run("uses custom TTL when provided", func(t *testing.T) { customTTL := 2 * time.Hour - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -127,7 +127,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProviderWithTTL(server.URL+"/api/plugins", customTTL) + provider := NewCatalogProviderWithTTL(server.URL+"/api/plugins", customTTL) result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) @@ -145,7 +145,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -153,34 +153,34 @@ func TestCloudProvider_GetMeta(t *testing.T) { }) } -func TestNewCloudProvider(t *testing.T) { +func TestNewCatalogProvider(t *testing.T) { t.Run("creates provider with default TTL", func(t *testing.T) { - provider := NewCloudProvider("https://grafana.com/api/plugins") - assert.Equal(t, defaultCloudTTL, provider.ttl) + provider := NewCatalogProvider("https://grafana.com/api/plugins") + assert.Equal(t, defaultCatalogTTL, provider.ttl) assert.NotNil(t, provider.httpClient) assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) t.Run("uses default URL when empty", func(t *testing.T) { - provider := NewCloudProvider("") + provider := NewCatalogProvider("") assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) } -func TestNewCloudProviderWithTTL(t *testing.T) { +func TestNewCatalogProviderWithTTL(t *testing.T) { t.Run("creates provider with custom TTL", func(t *testing.T) { customTTL := 2 * time.Hour - provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", customTTL) + provider := NewCatalogProviderWithTTL("https://grafana.com/api/plugins", customTTL) assert.Equal(t, customTTL, provider.ttl) }) t.Run("accepts zero TTL", func(t *testing.T) { - provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", 0) + provider := NewCatalogProviderWithTTL("https://grafana.com/api/plugins", 0) assert.Equal(t, time.Duration(0), provider.ttl) }) t.Run("uses default URL when empty", func(t *testing.T) { - provider := NewCloudProviderWithTTL("", defaultCloudTTL) + provider := NewCatalogProviderWithTTL("", defaultCatalogTTL) assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) } diff --git a/apps/plugins/pkg/app/meta/cloud.go b/apps/plugins/pkg/app/meta/cloud.go deleted file mode 100644 index f49445d093a..00000000000 --- a/apps/plugins/pkg/app/meta/cloud.go +++ /dev/null @@ -1,131 +0,0 @@ -package meta - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "path" - "time" - - "github.com/grafana/grafana-app-sdk/logging" - - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" -) - -const ( - defaultCloudTTL = 1 * time.Hour -) - -// CloudProvider retrieves plugin metadata from the grafana.com API. -type CloudProvider struct { - httpClient *http.Client - grafanaComAPIURL string - log logging.Logger - ttl time.Duration -} - -// NewCloudProvider creates a new CloudProvider that fetches metadata from grafana.com. -func NewCloudProvider(grafanaComAPIURL string) *CloudProvider { - return NewCloudProviderWithTTL(grafanaComAPIURL, defaultCloudTTL) -} - -// NewCloudProviderWithTTL creates a new CloudProvider with a custom TTL. -func NewCloudProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CloudProvider { - if grafanaComAPIURL == "" { - grafanaComAPIURL = "https://grafana.com/api/plugins" - } - - return &CloudProvider{ - httpClient: &http.Client{ - Timeout: 10 * time.Second, - }, - grafanaComAPIURL: grafanaComAPIURL, - log: logging.DefaultLogger, - ttl: ttl, - } -} - -// GetMeta fetches plugin metadata from grafana.com API endpoint: -// GET /api/plugins/{pluginId}/versions/{version} -func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { - u, err := url.Parse(p.grafanaComAPIURL) - if err != nil { - return nil, fmt.Errorf("invalid grafana.com API URL: %w", err) - } - - u.Path = path.Join(u.Path, pluginID, "versions", version) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "grafana-plugins-app") - - resp, err := p.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to fetch plugin metadata: %w", err) - } - defer func() { - if err = resp.Body.Close(); err != nil { - p.log.Warn("Failed to close response body", "error", err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return nil, ErrMetaNotFound - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code %d from grafana.com API", resp.StatusCode) - } - - var gcomMeta grafanaComPluginVersionMeta - if err = json.NewDecoder(resp.Body).Decode(&gcomMeta); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &Result{ - Meta: gcomMeta.JSON, - TTL: p.ttl, - }, nil -} - -// grafanaComPluginVersionMeta represents the response from grafana.com API -// GET /api/plugins/{pluginId}/versions/{version} -type grafanaComPluginVersionMeta struct { - PluginID string `json:"pluginSlug"` - Version string `json:"version"` - URL string `json:"url"` - Commit string `json:"commit"` - Description string `json:"description"` - Keywords []string `json:"keywords"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"` - Readme string `json:"readme"` - Downloads int `json:"downloads"` - Verified bool `json:"verified"` - Status string `json:"status"` - StatusContext string `json:"statusContext"` - DownloadSlug string `json:"downloadSlug"` - SignatureType string `json:"signatureType"` - SignedByOrg string `json:"signedByOrg"` - SignedByOrgName string `json:"signedByOrgName"` - Packages struct { - Any struct { - Md5 string `json:"md5"` - Sha256 string `json:"sha256"` - PackageName string `json:"packageName"` - DownloadURL string `json:"downloadUrl"` - } `json:"any"` - } `json:"packages"` - Links []struct { - Rel string `json:"rel"` - Href string `json:"href"` - } `json:"links"` - AngularDetected bool `json:"angularDetected"` - Scopes []string `json:"scopes"` -} diff --git a/apps/plugins/pkg/app/meta/converter.go b/apps/plugins/pkg/app/meta/converter.go new file mode 100644 index 00000000000..28180a263bd --- /dev/null +++ b/apps/plugins/pkg/app/meta/converter.go @@ -0,0 +1,744 @@ +package meta + +import ( + "encoding/json" + "time" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData. +// nolint:gocyclo +func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData { + meta := pluginsv0alpha1.MetaJSONData{ + Id: jsonData.ID, + Name: jsonData.Name, + } + + // Map plugin type + switch jsonData.Type { + case plugins.TypeApp: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp + case plugins.TypeDataSource: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource + case plugins.TypePanel: + meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel + case plugins.TypeRenderer: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer + } + + // Map Info + meta.Info = pluginsv0alpha1.MetaInfo{ + Keywords: jsonData.Info.Keywords, + Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{ + Small: jsonData.Info.Logos.Small, + Large: jsonData.Info.Logos.Large, + }, + Updated: jsonData.Info.Updated, + Version: jsonData.Info.Version, + } + + if jsonData.Info.Description != "" { + meta.Info.Description = &jsonData.Info.Description + } + + if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" { + author := &pluginsv0alpha1.MetaV0alpha1InfoAuthor{} + if jsonData.Info.Author.Name != "" { + author.Name = &jsonData.Info.Author.Name + } + if jsonData.Info.Author.URL != "" { + author.Url = &jsonData.Info.Author.URL + } + meta.Info.Author = author + } + + if len(jsonData.Info.Links) > 0 { + meta.Info.Links = make([]pluginsv0alpha1.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) + for _, link := range jsonData.Info.Links { + v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{} + if link.Name != "" { + v0Link.Name = &link.Name + } + if link.URL != "" { + v0Link.Url = &link.URL + } + meta.Info.Links = append(meta.Info.Links, v0Link) + } + } + + if len(jsonData.Info.Screenshots) > 0 { + meta.Info.Screenshots = make([]pluginsv0alpha1.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) + for _, screenshot := range jsonData.Info.Screenshots { + v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{} + if screenshot.Name != "" { + v0Screenshot.Name = &screenshot.Name + } + if screenshot.Path != "" { + v0Screenshot.Path = &screenshot.Path + } + meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot) + } + } + + // Map Dependencies + meta.Dependencies = pluginsv0alpha1.MetaDependencies{ + GrafanaDependency: jsonData.Dependencies.GrafanaDependency, + } + + if jsonData.Dependencies.GrafanaVersion != "" { + meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion + } + + if len(jsonData.Dependencies.Plugins) > 0 { + meta.Dependencies.Plugins = make([]pluginsv0alpha1.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) + for _, dep := range jsonData.Dependencies.Plugins { + var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType + switch dep.Type { + case "app": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp + case "datasource": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource + case "panel": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel + } + meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{ + Id: dep.ID, + Type: depType, + Name: dep.Name, + }) + } + } + + if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { + meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{ + ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents, + } + } + + // Map optional boolean fields + if jsonData.Alerting { + meta.Alerting = &jsonData.Alerting + } + if jsonData.Annotations { + meta.Annotations = &jsonData.Annotations + } + if jsonData.AutoEnabled { + meta.AutoEnabled = &jsonData.AutoEnabled + } + if jsonData.Backend { + meta.Backend = &jsonData.Backend + } + if jsonData.BuiltIn { + meta.BuiltIn = &jsonData.BuiltIn + } + if jsonData.HideFromList { + meta.HideFromList = &jsonData.HideFromList + } + if jsonData.Logs { + meta.Logs = &jsonData.Logs + } + if jsonData.Metrics { + meta.Metrics = &jsonData.Metrics + } + if jsonData.MultiValueFilterOperators { + meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators + } + if jsonData.Preload { + meta.Preload = &jsonData.Preload + } + if jsonData.SkipDataQuery { + meta.SkipDataQuery = &jsonData.SkipDataQuery + } + if jsonData.Streaming { + meta.Streaming = &jsonData.Streaming + } + if jsonData.Tracing { + meta.Tracing = &jsonData.Tracing + } + + // Map category + if jsonData.Category != "" { + var category pluginsv0alpha1.MetaJSONDataCategory + switch jsonData.Category { + case "tsdb": + category = pluginsv0alpha1.MetaJSONDataCategoryTsdb + case "logging": + category = pluginsv0alpha1.MetaJSONDataCategoryLogging + case "cloud": + category = pluginsv0alpha1.MetaJSONDataCategoryCloud + case "tracing": + category = pluginsv0alpha1.MetaJSONDataCategoryTracing + case "profiling": + category = pluginsv0alpha1.MetaJSONDataCategoryProfiling + case "sql": + category = pluginsv0alpha1.MetaJSONDataCategorySql + case "enterprise": + category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise + case "iot": + category = pluginsv0alpha1.MetaJSONDataCategoryIot + case "other": + category = pluginsv0alpha1.MetaJSONDataCategoryOther + default: + category = pluginsv0alpha1.MetaJSONDataCategoryOther + } + meta.Category = &category + } + + // Map state + if jsonData.State != "" { + var state pluginsv0alpha1.MetaJSONDataState + switch jsonData.State { + case plugins.ReleaseStateAlpha: + state = pluginsv0alpha1.MetaJSONDataStateAlpha + case plugins.ReleaseStateBeta: + state = pluginsv0alpha1.MetaJSONDataStateBeta + default: + } + if state != "" { + meta.State = &state + } + } + + // Map executable + if jsonData.Executable != "" { + meta.Executable = &jsonData.Executable + } + + // Map QueryOptions + if len(jsonData.QueryOptions) > 0 { + queryOptions := &pluginsv0alpha1.MetaQueryOptions{} + if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok { + queryOptions.MaxDataPoints = &val + } + if val, ok := jsonData.QueryOptions["minInterval"]; ok { + queryOptions.MinInterval = &val + } + if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok { + queryOptions.CacheTimeout = &val + } + meta.QueryOptions = queryOptions + } + + // Map Includes + if len(jsonData.Includes) > 0 { + meta.Includes = make([]pluginsv0alpha1.MetaInclude, 0, len(jsonData.Includes)) + for _, include := range jsonData.Includes { + v0Include := pluginsv0alpha1.MetaInclude{} + if include.UID != "" { + v0Include.Uid = &include.UID + } + if include.Type != "" { + var includeType pluginsv0alpha1.MetaIncludeType + switch include.Type { + case "dashboard": + includeType = pluginsv0alpha1.MetaIncludeTypeDashboard + case "page": + includeType = pluginsv0alpha1.MetaIncludeTypePage + case "panel": + includeType = pluginsv0alpha1.MetaIncludeTypePanel + case "datasource": + includeType = pluginsv0alpha1.MetaIncludeTypeDatasource + } + v0Include.Type = &includeType + } + if include.Name != "" { + v0Include.Name = &include.Name + } + if include.Component != "" { + v0Include.Component = &include.Component + } + if include.Role != "" { + var role pluginsv0alpha1.MetaIncludeRole + switch include.Role { + case "Admin": + role = pluginsv0alpha1.MetaIncludeRoleAdmin + case "Editor": + role = pluginsv0alpha1.MetaIncludeRoleEditor + case "Viewer": + role = pluginsv0alpha1.MetaIncludeRoleViewer + } + v0Include.Role = &role + } + if include.Action != "" { + v0Include.Action = &include.Action + } + if include.Path != "" { + v0Include.Path = &include.Path + } + if include.AddToNav { + v0Include.AddToNav = &include.AddToNav + } + if include.DefaultNav { + v0Include.DefaultNav = &include.DefaultNav + } + if include.Icon != "" { + v0Include.Icon = &include.Icon + } + meta.Includes = append(meta.Includes, v0Include) + } + } + + // Map Routes + if len(jsonData.Routes) > 0 { + meta.Routes = make([]pluginsv0alpha1.MetaRoute, 0, len(jsonData.Routes)) + for _, route := range jsonData.Routes { + v0Route := pluginsv0alpha1.MetaRoute{} + if route.Path != "" { + v0Route.Path = &route.Path + } + if route.Method != "" { + v0Route.Method = &route.Method + } + if route.URL != "" { + v0Route.Url = &route.URL + } + if route.ReqRole != "" { + reqRole := string(route.ReqRole) + v0Route.ReqRole = &reqRole + } + if route.ReqAction != "" { + v0Route.ReqAction = &route.ReqAction + } + if len(route.Headers) > 0 { + headers := make([]string, 0, len(route.Headers)) + for _, header := range route.Headers { + headers = append(headers, header.Name+": "+header.Content) + } + v0Route.Headers = headers + } + if len(route.URLParams) > 0 { + v0Route.UrlParams = make([]pluginsv0alpha1.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) + for _, param := range route.URLParams { + v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{} + if param.Name != "" { + v0Param.Name = ¶m.Name + } + if param.Content != "" { + v0Param.Content = ¶m.Content + } + v0Route.UrlParams = append(v0Route.UrlParams, v0Param) + } + } + if route.TokenAuth != nil { + v0Route.TokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteTokenAuth{} + if route.TokenAuth.Url != "" { + v0Route.TokenAuth.Url = &route.TokenAuth.Url + } + if len(route.TokenAuth.Scopes) > 0 { + v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes + } + if len(route.TokenAuth.Params) > 0 { + v0Route.TokenAuth.Params = make(map[string]interface{}) + for k, v := range route.TokenAuth.Params { + v0Route.TokenAuth.Params[k] = v + } + } + } + if route.JwtTokenAuth != nil { + v0Route.JwtTokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteJwtTokenAuth{} + if route.JwtTokenAuth.Url != "" { + v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url + } + if len(route.JwtTokenAuth.Scopes) > 0 { + v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes + } + if len(route.JwtTokenAuth.Params) > 0 { + v0Route.JwtTokenAuth.Params = make(map[string]interface{}) + for k, v := range route.JwtTokenAuth.Params { + v0Route.JwtTokenAuth.Params[k] = v + } + } + } + if len(route.Body) > 0 { + var bodyMap map[string]interface{} + if err := json.Unmarshal(route.Body, &bodyMap); err == nil { + v0Route.Body = bodyMap + } + } + meta.Routes = append(meta.Routes, v0Route) + } + } + + // Map Extensions + if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 || + len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 { + extensions := &pluginsv0alpha1.MetaExtensions{} + + if len(jsonData.Extensions.AddedLinks) > 0 { + extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) + for _, link := range jsonData.Extensions.AddedLinks { + v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{ + Targets: link.Targets, + Title: link.Title, + } + if link.Description != "" { + v0Link.Description = &link.Description + } + extensions.AddedLinks = append(extensions.AddedLinks, v0Link) + } + } + + if len(jsonData.Extensions.AddedComponents) > 0 { + extensions.AddedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) + for _, comp := range jsonData.Extensions.AddedComponents { + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{ + Targets: comp.Targets, + Title: comp.Title, + } + if comp.Description != "" { + v0Comp.Description = &comp.Description + } + extensions.AddedComponents = append(extensions.AddedComponents, v0Comp) + } + } + + if len(jsonData.Extensions.ExposedComponents) > 0 { + extensions.ExposedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) + for _, comp := range jsonData.Extensions.ExposedComponents { + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{ + Id: comp.Id, + } + if comp.Title != "" { + v0Comp.Title = &comp.Title + } + if comp.Description != "" { + v0Comp.Description = &comp.Description + } + extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp) + } + } + + if len(jsonData.Extensions.ExtensionPoints) > 0 { + extensions.ExtensionPoints = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) + for _, point := range jsonData.Extensions.ExtensionPoints { + v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{ + Id: point.Id, + } + if point.Title != "" { + v0Point.Title = &point.Title + } + if point.Description != "" { + v0Point.Description = &point.Description + } + extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point) + } + } + + meta.Extensions = extensions + } + + // Map Roles + if len(jsonData.Roles) > 0 { + meta.Roles = make([]pluginsv0alpha1.MetaRole, 0, len(jsonData.Roles)) + for _, role := range jsonData.Roles { + v0Role := pluginsv0alpha1.MetaRole{ + Grants: role.Grants, + } + if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { + v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{} + if role.Role.Name != "" { + v0RoleRole.Name = &role.Role.Name + } + if role.Role.Description != "" { + v0RoleRole.Description = &role.Role.Description + } + if len(role.Role.Permissions) > 0 { + v0RoleRole.Permissions = make([]pluginsv0alpha1.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) + for _, perm := range role.Role.Permissions { + v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{} + if perm.Action != "" { + v0Perm.Action = &perm.Action + } + if perm.Scope != "" { + v0Perm.Scope = &perm.Scope + } + v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm) + } + } + v0Role.Role = v0RoleRole + } + meta.Roles = append(meta.Roles, v0Role) + } + } + + // Map IAM + if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 { + iam := &pluginsv0alpha1.MetaIAM{ + Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), + } + for _, perm := range jsonData.IAM.Permissions { + v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{} + if perm.Action != "" { + v0Perm.Action = &perm.Action + } + if perm.Scope != "" { + v0Perm.Scope = &perm.Scope + } + iam.Permissions = append(iam.Permissions, v0Perm) + } + meta.Iam = iam + } + + return meta +} + +// pluginStorePluginToMeta converts a pluginstore.Plugin to a pluginsv0alpha1.MetaSpec. +// This is similar to pluginToPluginMetaSpec but works with the plugin store DTO. +// loadingStrategy and moduleHash are optional calculated values that can be provided. +func pluginStorePluginToMeta(plugin pluginstore.Plugin, loadingStrategy plugins.LoadingStrategy, moduleHash string) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: jsonDataToMetaJSONData(plugin.JSONData), + } + + // Set Class - default to External if not specified + var c pluginsv0alpha1.MetaSpecClass + if plugin.Class == plugins.ClassCore { + c = pluginsv0alpha1.MetaSpecClassCore + } else { + c = pluginsv0alpha1.MetaSpecClassExternal + } + metaSpec.Class = c + + if plugin.Module != "" { + module := &pluginsv0alpha1.MetaV0alpha1SpecModule{ + Path: plugin.Module, + } + if moduleHash != "" { + module.Hash = &moduleHash + } + if loadingStrategy != "" { + var ls pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategy + switch loadingStrategy { + case plugins.LoadingStrategyFetch: + ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyFetch + case plugins.LoadingStrategyScript: + ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript + } + module.LoadingStrategy = &ls + } + metaSpec.Module = module + } + + if plugin.BaseURL != "" { + metaSpec.BaseURL = &plugin.BaseURL + } + + if plugin.Signature != "" { + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: convertSignatureStatus(plugin.Signature), + } + + if plugin.SignatureType != "" { + sigType := convertSignatureType(plugin.SignatureType) + signature.Type = &sigType + } + + if plugin.SignatureOrg != "" { + signature.Org = &plugin.SignatureOrg + } + + metaSpec.Signature = signature + } + + if len(plugin.Children) > 0 { + metaSpec.Children = plugin.Children + } + + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: plugin.Angular.Detected, + } + + if len(plugin.Translations) > 0 { + metaSpec.Translations = plugin.Translations + } + + return metaSpec +} + +// convertSignatureStatus converts plugins.SignatureStatus to pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus. +func convertSignatureStatus(status plugins.SignatureStatus) pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus { + switch status { + case plugins.SignatureStatusInternal: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInternal + case plugins.SignatureStatusValid: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid + case plugins.SignatureStatusInvalid: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInvalid + case plugins.SignatureStatusModified: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusModified + case plugins.SignatureStatusUnsigned: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned + default: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned + } +} + +// convertSignatureType converts plugins.SignatureType to pluginsv0alpha1.MetaV0alpha1SpecSignatureType. +func convertSignatureType(sigType plugins.SignatureType) pluginsv0alpha1.MetaV0alpha1SpecSignatureType { + switch sigType { + case plugins.SignatureTypeGrafana: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + case plugins.SignatureTypeCommercial: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial + case plugins.SignatureTypeCommunity: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity + case plugins.SignatureTypePrivate: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate + case plugins.SignatureTypePrivateGlob: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob + default: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + } +} + +// pluginToMetaSpec converts a fully loaded *plugins.Plugin to a pluginsv0alpha1.MetaSpec. +func pluginToMetaSpec(plugin *plugins.Plugin) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: jsonDataToMetaJSONData(plugin.JSONData), + } + + // Set Class - default to External if not specified + var c pluginsv0alpha1.MetaSpecClass + if plugin.Class == plugins.ClassCore { + c = pluginsv0alpha1.MetaSpecClassCore + } else { + c = pluginsv0alpha1.MetaSpecClassExternal + } + metaSpec.Class = c + + // Set module information + if plugin.Module != "" { + module := &pluginsv0alpha1.MetaV0alpha1SpecModule{ + Path: plugin.Module, + } + + loadingStrategy := pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript + module.LoadingStrategy = &loadingStrategy + + metaSpec.Module = module + } + + // Set BaseURL + if plugin.BaseURL != "" { + metaSpec.BaseURL = &plugin.BaseURL + } + + // Set signature information + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: convertSignatureStatus(plugin.Signature), + } + + if plugin.SignatureType != "" { + sigType := convertSignatureType(plugin.SignatureType) + signature.Type = &sigType + } + + if plugin.SignatureOrg != "" { + signature.Org = &plugin.SignatureOrg + } + + metaSpec.Signature = signature + + if len(plugin.Children) > 0 { + children := make([]string, 0, len(plugin.Children)) + for _, child := range plugin.Children { + children = append(children, child.ID) + } + metaSpec.Children = children + } + + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: plugin.Angular.Detected, + } + + if len(plugin.Translations) > 0 { + metaSpec.Translations = plugin.Translations + } + + return metaSpec +} + +// grafanaComPluginVersionMeta represents the response from grafana.com API +// GET /api/plugins/{pluginId}/versions/{version} +type grafanaComPluginVersionMeta struct { + PluginID string `json:"pluginSlug"` + Version string `json:"version"` + URL string `json:"url"` + Commit string `json:"commit"` + Description string `json:"description"` + Keywords []string `json:"keywords"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + JSON pluginsv0alpha1.MetaJSONData `json:"json"` + Readme string `json:"readme"` + Downloads int `json:"downloads"` + Verified bool `json:"verified"` + Status string `json:"status"` + StatusContext string `json:"statusContext"` + DownloadSlug string `json:"downloadSlug"` + SignatureType string `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` + Packages struct { + Any struct { + Md5 string `json:"md5"` + Sha256 string `json:"sha256"` + PackageName string `json:"packageName"` + DownloadURL string `json:"downloadUrl"` + } `json:"any"` + } `json:"packages"` + Links []struct { + Rel string `json:"rel"` + Href string `json:"href"` + } `json:"links"` + AngularDetected bool `json:"angularDetected"` + Scopes []string `json:"scopes"` +} + +// grafanaComPluginVersionMetaToMetaSpec converts a grafanaComPluginVersionMeta to a pluginsv0alpha1.MetaSpec. +func grafanaComPluginVersionMetaToMetaSpec(gcomMeta grafanaComPluginVersionMeta) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: gcomMeta.JSON, + Class: pluginsv0alpha1.MetaSpecClassExternal, + } + + if gcomMeta.SignatureType != "" { + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid, + } + + switch gcomMeta.SignatureType { + case "grafana": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + signature.Type = &sigType + case "commercial": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial + signature.Type = &sigType + case "community": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity + signature.Type = &sigType + case "private": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate + signature.Type = &sigType + case "private-glob": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob + signature.Type = &sigType + } + + if gcomMeta.SignedByOrg != "" { + signature.Org = &gcomMeta.SignedByOrg + } + + metaSpec.Signature = signature + } + + // Set angular info + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: gcomMeta.AngularDetected, + } + + return metaSpec +} diff --git a/apps/plugins/pkg/app/meta/core.go b/apps/plugins/pkg/app/meta/core.go index 3452f95b24b..9fb167d5578 100644 --- a/apps/plugins/pkg/app/meta/core.go +++ b/apps/plugins/pkg/app/meta/core.go @@ -2,7 +2,6 @@ package meta import ( "context" - "encoding/json" "errors" "os" "path/filepath" @@ -13,7 +12,15 @@ import ( pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) const ( @@ -23,9 +30,10 @@ const ( // CoreProvider retrieves plugin metadata for core plugins. type CoreProvider struct { mu sync.RWMutex - loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData + loadedPlugins map[string]pluginsv0alpha1.MetaSpec initialized bool ttl time.Duration + loader pluginsLoader.Service } // NewCoreProvider creates a new CoreProvider for core plugins. @@ -35,9 +43,13 @@ func NewCoreProvider() *CoreProvider { // NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL. func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider { + cfg := &config.PluginManagementCfg{ + Features: config.Features{}, + } return &CoreProvider{ - loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData), + loadedPlugins: make(map[string]pluginsv0alpha1.MetaSpec), ttl: ttl, + loader: createLoader(cfg), } } @@ -76,9 +88,9 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result p.initialized = true } - if meta, found := p.loadedPlugins[pluginID]; found { + if spec, found := p.loadedPlugins[pluginID]; found { return &Result{ - Meta: meta, + Meta: spec, TTL: p.ttl, }, nil } @@ -86,8 +98,8 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result return nil, ErrMetaNotFound } -// loadPlugins discovers and caches all core plugins. -// Returns an error if the static root path cannot be found or if plugin discovery fails. +// loadPlugins discovers and caches all core plugins by fully loading them. +// Returns an error if the static root path cannot be found or if plugin loading fails. // This error will be handled gracefully by GetMeta, which will return ErrMetaNotFound // to allow other providers to handle the request. func (p *CoreProvider) loadPlugins(ctx context.Context) error { @@ -108,496 +120,51 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error { panelPath := filepath.Join(staticRootPath, "app", "plugins", "panel") src := sources.NewLocalSource(plugins.ClassCore, []string{datasourcePath, panelPath}) - ps, err := src.Discover(ctx) + loadedPlugins, err := p.loader.Load(ctx, src) if err != nil { return err } - if len(ps) == 0 { - logging.DefaultLogger.Warn("CoreProvider: no core plugins found during discovery") + if len(loadedPlugins) == 0 { + logging.DefaultLogger.Warn("CoreProvider: no core plugins found during loading") return nil } - for _, bundle := range ps { - meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData) - p.loadedPlugins[bundle.Primary.JSONData.ID] = meta + for _, plugin := range loadedPlugins { + metaSpec := pluginToMetaSpec(plugin) + p.loadedPlugins[plugin.ID] = metaSpec } return nil } -// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData. -// nolint:gocyclo -func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData { - meta := pluginsv0alpha1.PluginMetaJSONData{ - Id: jsonData.ID, - Name: jsonData.Name, - } - - // Map plugin type - switch jsonData.Type { - case plugins.TypeApp: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp - case plugins.TypeDataSource: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource - case plugins.TypePanel: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel - case plugins.TypeRenderer: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer - } - - // Map Info - meta.Info = pluginsv0alpha1.PluginMetaInfo{ - Keywords: jsonData.Info.Keywords, - Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{ - Small: jsonData.Info.Logos.Small, - Large: jsonData.Info.Logos.Large, +// createLoader creates a loader service configured for core plugins. +func createLoader(cfg *config.PluginManagementCfg) pluginsLoader.Service { + d := discovery.New(cfg, discovery.Opts{ + FilterFuncs: []discovery.FilterFunc{ + // Allow all plugin types for core plugins }, - Updated: jsonData.Info.Updated, - Version: jsonData.Info.Version, - } + }) + b := bootstrap.New(cfg, bootstrap.Opts{ + DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required for metadata + }) + v := validation.New(cfg, validation.Opts{ + ValidateFuncs: []validation.ValidateFunc{ + // Skip validation for core plugins - they're trusted + }, + }) + i := initialization.New(cfg, initialization.Opts{ + InitializeFuncs: []initialization.InitializeFunc{ + // Skip initialization - we only need metadata, not running plugins + }, + }) + t, _ := termination.New(cfg, termination.Opts{ + TerminateFuncs: []termination.TerminateFunc{ + // No termination needed for metadata-only loading + }, + }) - if jsonData.Info.Description != "" { - meta.Info.Description = &jsonData.Info.Description - } + et := pluginerrs.ProvideErrorTracker() - if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" { - author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{} - if jsonData.Info.Author.Name != "" { - author.Name = &jsonData.Info.Author.Name - } - if jsonData.Info.Author.URL != "" { - author.Url = &jsonData.Info.Author.URL - } - meta.Info.Author = author - } - - if len(jsonData.Info.Links) > 0 { - meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) - for _, link := range jsonData.Info.Links { - v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{} - if link.Name != "" { - v0Link.Name = &link.Name - } - if link.URL != "" { - v0Link.Url = &link.URL - } - meta.Info.Links = append(meta.Info.Links, v0Link) - } - } - - if len(jsonData.Info.Screenshots) > 0 { - meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) - for _, screenshot := range jsonData.Info.Screenshots { - v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{} - if screenshot.Name != "" { - v0Screenshot.Name = &screenshot.Name - } - if screenshot.Path != "" { - v0Screenshot.Path = &screenshot.Path - } - meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot) - } - } - - // Map Dependencies - meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{ - GrafanaDependency: jsonData.Dependencies.GrafanaDependency, - } - - if jsonData.Dependencies.GrafanaVersion != "" { - meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion - } - - if len(jsonData.Dependencies.Plugins) > 0 { - meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) - for _, dep := range jsonData.Dependencies.Plugins { - var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType - switch dep.Type { - case "app": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp - case "datasource": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource - case "panel": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel - } - meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{ - Id: dep.ID, - Type: depType, - Name: dep.Name, - }) - } - } - - if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { - meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{ - ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents, - } - } - - // Map optional boolean fields - if jsonData.Alerting { - meta.Alerting = &jsonData.Alerting - } - if jsonData.Annotations { - meta.Annotations = &jsonData.Annotations - } - if jsonData.AutoEnabled { - meta.AutoEnabled = &jsonData.AutoEnabled - } - if jsonData.Backend { - meta.Backend = &jsonData.Backend - } - if jsonData.BuiltIn { - meta.BuiltIn = &jsonData.BuiltIn - } - if jsonData.HideFromList { - meta.HideFromList = &jsonData.HideFromList - } - if jsonData.Logs { - meta.Logs = &jsonData.Logs - } - if jsonData.Metrics { - meta.Metrics = &jsonData.Metrics - } - if jsonData.MultiValueFilterOperators { - meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators - } - if jsonData.Preload { - meta.Preload = &jsonData.Preload - } - if jsonData.SkipDataQuery { - meta.SkipDataQuery = &jsonData.SkipDataQuery - } - if jsonData.Streaming { - meta.Streaming = &jsonData.Streaming - } - if jsonData.Tracing { - meta.Tracing = &jsonData.Tracing - } - - // Map category - if jsonData.Category != "" { - var category pluginsv0alpha1.PluginMetaJSONDataCategory - switch jsonData.Category { - case "tsdb": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb - case "logging": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging - case "cloud": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud - case "tracing": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing - case "profiling": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling - case "sql": - category = pluginsv0alpha1.PluginMetaJSONDataCategorySql - case "enterprise": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise - case "iot": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot - case "other": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther - default: - category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther - } - meta.Category = &category - } - - // Map state - if jsonData.State != "" { - var state pluginsv0alpha1.PluginMetaJSONDataState - switch jsonData.State { - case plugins.ReleaseStateAlpha: - state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha - case plugins.ReleaseStateBeta: - state = pluginsv0alpha1.PluginMetaJSONDataStateBeta - default: - } - if state != "" { - meta.State = &state - } - } - - // Map executable - if jsonData.Executable != "" { - meta.Executable = &jsonData.Executable - } - - // Map QueryOptions - if len(jsonData.QueryOptions) > 0 { - queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{} - if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok { - queryOptions.MaxDataPoints = &val - } - if val, ok := jsonData.QueryOptions["minInterval"]; ok { - queryOptions.MinInterval = &val - } - if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok { - queryOptions.CacheTimeout = &val - } - meta.QueryOptions = queryOptions - } - - // Map Includes - if len(jsonData.Includes) > 0 { - meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes)) - for _, include := range jsonData.Includes { - v0Include := pluginsv0alpha1.PluginMetaInclude{} - if include.UID != "" { - v0Include.Uid = &include.UID - } - if include.Type != "" { - var includeType pluginsv0alpha1.PluginMetaIncludeType - switch include.Type { - case "dashboard": - includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard - case "page": - includeType = pluginsv0alpha1.PluginMetaIncludeTypePage - case "panel": - includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel - case "datasource": - includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource - } - v0Include.Type = &includeType - } - if include.Name != "" { - v0Include.Name = &include.Name - } - if include.Component != "" { - v0Include.Component = &include.Component - } - if include.Role != "" { - var role pluginsv0alpha1.PluginMetaIncludeRole - switch include.Role { - case "Admin": - role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin - case "Editor": - role = pluginsv0alpha1.PluginMetaIncludeRoleEditor - case "Viewer": - role = pluginsv0alpha1.PluginMetaIncludeRoleViewer - } - v0Include.Role = &role - } - if include.Action != "" { - v0Include.Action = &include.Action - } - if include.Path != "" { - v0Include.Path = &include.Path - } - if include.AddToNav { - v0Include.AddToNav = &include.AddToNav - } - if include.DefaultNav { - v0Include.DefaultNav = &include.DefaultNav - } - if include.Icon != "" { - v0Include.Icon = &include.Icon - } - meta.Includes = append(meta.Includes, v0Include) - } - } - - // Map Routes - if len(jsonData.Routes) > 0 { - meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes)) - for _, route := range jsonData.Routes { - v0Route := pluginsv0alpha1.PluginMetaRoute{} - if route.Path != "" { - v0Route.Path = &route.Path - } - if route.Method != "" { - v0Route.Method = &route.Method - } - if route.URL != "" { - v0Route.Url = &route.URL - } - if route.ReqRole != "" { - reqRole := string(route.ReqRole) - v0Route.ReqRole = &reqRole - } - if route.ReqAction != "" { - v0Route.ReqAction = &route.ReqAction - } - if len(route.Headers) > 0 { - headers := make([]string, 0, len(route.Headers)) - for _, header := range route.Headers { - headers = append(headers, header.Name+": "+header.Content) - } - v0Route.Headers = headers - } - if len(route.URLParams) > 0 { - v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) - for _, param := range route.URLParams { - v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{} - if param.Name != "" { - v0Param.Name = ¶m.Name - } - if param.Content != "" { - v0Param.Content = ¶m.Content - } - v0Route.UrlParams = append(v0Route.UrlParams, v0Param) - } - } - if route.TokenAuth != nil { - v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{} - if route.TokenAuth.Url != "" { - v0Route.TokenAuth.Url = &route.TokenAuth.Url - } - if len(route.TokenAuth.Scopes) > 0 { - v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes - } - if len(route.TokenAuth.Params) > 0 { - v0Route.TokenAuth.Params = make(map[string]interface{}) - for k, v := range route.TokenAuth.Params { - v0Route.TokenAuth.Params[k] = v - } - } - } - if route.JwtTokenAuth != nil { - v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{} - if route.JwtTokenAuth.Url != "" { - v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url - } - if len(route.JwtTokenAuth.Scopes) > 0 { - v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes - } - if len(route.JwtTokenAuth.Params) > 0 { - v0Route.JwtTokenAuth.Params = make(map[string]interface{}) - for k, v := range route.JwtTokenAuth.Params { - v0Route.JwtTokenAuth.Params[k] = v - } - } - } - if len(route.Body) > 0 { - var bodyMap map[string]interface{} - if err := json.Unmarshal(route.Body, &bodyMap); err == nil { - v0Route.Body = bodyMap - } - } - meta.Routes = append(meta.Routes, v0Route) - } - } - - // Map Extensions - if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 || - len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 { - extensions := &pluginsv0alpha1.PluginMetaExtensions{} - - if len(jsonData.Extensions.AddedLinks) > 0 { - extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) - for _, link := range jsonData.Extensions.AddedLinks { - v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{ - Targets: link.Targets, - Title: link.Title, - } - if link.Description != "" { - v0Link.Description = &link.Description - } - extensions.AddedLinks = append(extensions.AddedLinks, v0Link) - } - } - - if len(jsonData.Extensions.AddedComponents) > 0 { - extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) - for _, comp := range jsonData.Extensions.AddedComponents { - v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{ - Targets: comp.Targets, - Title: comp.Title, - } - if comp.Description != "" { - v0Comp.Description = &comp.Description - } - extensions.AddedComponents = append(extensions.AddedComponents, v0Comp) - } - } - - if len(jsonData.Extensions.ExposedComponents) > 0 { - extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) - for _, comp := range jsonData.Extensions.ExposedComponents { - v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{ - Id: comp.Id, - } - if comp.Title != "" { - v0Comp.Title = &comp.Title - } - if comp.Description != "" { - v0Comp.Description = &comp.Description - } - extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp) - } - } - - if len(jsonData.Extensions.ExtensionPoints) > 0 { - extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) - for _, point := range jsonData.Extensions.ExtensionPoints { - v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{ - Id: point.Id, - } - if point.Title != "" { - v0Point.Title = &point.Title - } - if point.Description != "" { - v0Point.Description = &point.Description - } - extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point) - } - } - - meta.Extensions = extensions - } - - // Map Roles - if len(jsonData.Roles) > 0 { - meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles)) - for _, role := range jsonData.Roles { - v0Role := pluginsv0alpha1.PluginMetaRole{ - Grants: role.Grants, - } - if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { - v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{} - if role.Role.Name != "" { - v0RoleRole.Name = &role.Role.Name - } - if role.Role.Description != "" { - v0RoleRole.Description = &role.Role.Description - } - if len(role.Role.Permissions) > 0 { - v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) - for _, perm := range role.Role.Permissions { - v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{} - if perm.Action != "" { - v0Perm.Action = &perm.Action - } - if perm.Scope != "" { - v0Perm.Scope = &perm.Scope - } - v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm) - } - } - v0Role.Role = v0RoleRole - } - meta.Roles = append(meta.Roles, v0Role) - } - } - - // Map IAM - if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 { - iam := &pluginsv0alpha1.PluginMetaIAM{ - Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), - } - for _, perm := range jsonData.IAM.Permissions { - v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{} - if perm.Action != "" { - v0Perm.Action = &perm.Action - } - if perm.Scope != "" { - v0Perm.Scope = &perm.Scope - } - iam.Permissions = append(iam.Permissions, v0Perm) - } - meta.Iam = iam - } - - return meta + return pluginsLoader.New(cfg, d, b, v, i, t, et) } diff --git a/apps/plugins/pkg/app/meta/core_test.go b/apps/plugins/pkg/app/meta/core_test.go index a9d7103e4a2..d6f51057fab 100644 --- a/apps/plugins/pkg/app/meta/core_test.go +++ b/apps/plugins/pkg/app/meta/core_test.go @@ -22,10 +22,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("returns cached plugin when available", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -58,10 +60,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("ignores version parameter", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -81,10 +85,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { customTTL := 2 * time.Hour provider := NewCoreProviderWithTTL(customTTL) - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -226,8 +232,8 @@ func TestCoreProvider_loadPlugins(t *testing.T) { if loaded { result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0") require.NoError(t, err) - assert.Equal(t, "test-datasource", result.Meta.Id) - assert.Equal(t, "Test Datasource", result.Meta.Name) + assert.Equal(t, "test-datasource", result.Meta.PluginJson.Id) + assert.Equal(t, "Test Datasource", result.Meta.PluginJson.Name) } }) } @@ -271,11 +277,11 @@ func TestJsonDataToMeta(t *testing.T) { }, } - meta := jsonDataToPluginMetaJSONData(jsonData) + meta := jsonDataToMetaJSONData(jsonData) assert.Equal(t, "test-plugin", meta.Id) assert.Equal(t, "Test Plugin", meta.Name) - assert.Equal(t, pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, meta.Type) + assert.Equal(t, pluginsv0alpha1.MetaJSONDataTypeDatasource, meta.Type) assert.Equal(t, "1.0.0", meta.Info.Version) assert.Equal(t, "Test description", *meta.Info.Description) assert.Equal(t, []string{"test", "plugin"}, meta.Info.Keywords) @@ -293,7 +299,7 @@ func TestJsonDataToMeta(t *testing.T) { }, } - meta := jsonDataToPluginMetaJSONData(jsonData) + meta := jsonDataToMetaJSONData(jsonData) assert.Nil(t, meta.Info.Description) assert.Nil(t, meta.Info.Author) diff --git a/apps/plugins/pkg/app/meta/local.go b/apps/plugins/pkg/app/meta/local.go new file mode 100644 index 00000000000..2c699520cfc --- /dev/null +++ b/apps/plugins/pkg/app/meta/local.go @@ -0,0 +1,53 @@ +package meta + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +const ( + defaultLocalTTL = 1 * time.Hour +) + +// PluginAssetsCalculator is an interface for calculating plugin asset information. +// LocalProvider requires this to calculate loading strategy and module hash. +type PluginAssetsCalculator interface { + LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy + ModuleHash(ctx context.Context, p pluginstore.Plugin) string +} + +// LocalProvider retrieves plugin metadata for locally installed plugins. +// It uses the plugin store to access plugins that have already been loaded. +type LocalProvider struct { + store pluginstore.Store + pluginAssets PluginAssetsCalculator +} + +// NewLocalProvider creates a new LocalProvider for locally installed plugins. +// pluginAssets is required for calculating loading strategy and module hash. +func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider { + return &LocalProvider{ + store: pluginStore, + pluginAssets: pluginAssets, + } +} + +// GetMeta retrieves plugin metadata for locally installed plugins. +func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { + plugin, exists := p.store.Plugin(ctx, pluginID) + if !exists { + return nil, ErrMetaNotFound + } + + loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin) + moduleHash := p.pluginAssets.ModuleHash(ctx, plugin) + + spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash) + return &Result{ + Meta: spec, + TTL: defaultLocalTTL, + }, nil +} diff --git a/apps/plugins/pkg/app/meta/manager.go b/apps/plugins/pkg/app/meta/manager.go index e694e88bba7..d8cd1f554c2 100644 --- a/apps/plugins/pkg/app/meta/manager.go +++ b/apps/plugins/pkg/app/meta/manager.go @@ -16,7 +16,7 @@ const ( // cachedMeta represents a cached metadata entry with expiration time type cachedMeta struct { - meta pluginsv0alpha1.PluginMetaJSONData + meta pluginsv0alpha1.MetaSpec ttl time.Duration expiresAt time.Time } @@ -84,7 +84,7 @@ func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string if err == nil { // Don't cache results with a zero TTL if result.TTL == 0 { - continue + return result, nil } pm.cacheMu.Lock() diff --git a/apps/plugins/pkg/app/meta/manager_test.go b/apps/plugins/pkg/app/meta/manager_test.go index 3d3fe3c936c..c9a8a9f3a5f 100644 --- a/apps/plugins/pkg/app/meta/manager_test.go +++ b/apps/plugins/pkg/app/meta/manager_test.go @@ -35,10 +35,12 @@ func TestProviderManager_GetMeta(t *testing.T) { ctx := context.Background() t.Run("returns cached result when available and not expired", func(t *testing.T) { - cachedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + cachedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider := &mockProvider{ @@ -60,8 +62,10 @@ func TestProviderManager_GetMeta(t *testing.T) { provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) { return &Result{ - Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"}, - TTL: time.Hour, + Meta: pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{Id: "different"}, + }, + TTL: time.Hour, }, nil } @@ -73,10 +77,12 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("fetches from provider when not cached", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } expectedTTL := 2 * time.Hour @@ -107,19 +113,16 @@ func TestProviderManager_GetMeta(t *testing.T) { assert.Equal(t, expectedTTL, cached.ttl) }) - t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) { - zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Zero TTL Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, - } - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + t.Run("does not cache result with zero TTL", func(t *testing.T) { + zeroTTLMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Zero TTL Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - provider1 := &mockProvider{ + provider := &mockProvider{ getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { return &Result{ Meta: zeroTTLMeta, @@ -127,37 +130,30 @@ func TestProviderManager_GetMeta(t *testing.T) { }, nil }, } - provider2 := &mockProvider{ - getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { - return &Result{ - Meta: expectedMeta, - TTL: time.Hour, - }, nil - }, - } - pm := NewProviderManager(provider1, provider2) + pm := NewProviderManager(provider) result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, zeroTTLMeta, result.Meta) + assert.Equal(t, time.Duration(0), result.TTL) pm.cacheMu.RLock() - cached, exists := pm.cache["test-plugin:1.0.0"] + _, exists := pm.cache["test-plugin:1.0.0"] pm.cacheMu.RUnlock() - assert.True(t, exists) - assert.Equal(t, expectedMeta, cached.meta) - assert.Equal(t, time.Hour, cached.ttl) + assert.False(t, exists, "zero TTL results should not be cached") }) t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider1 := &mockProvider{ @@ -229,15 +225,19 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("skips expired cache entries", func(t *testing.T) { - expiredMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Expired Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expiredMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Expired Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } callCount := 0 @@ -272,15 +272,19 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("uses first successful provider", func(t *testing.T) { - expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Provider 1 Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta1 := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Provider 1 Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{ - Id: "test-plugin", - Name: "Provider 2 Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + expectedMeta2 := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Provider 2 Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider1 := &mockProvider{ @@ -331,9 +335,9 @@ func TestProviderManager_Run(t *testing.T) { func TestProviderManager_cleanupExpired(t *testing.T) { t.Run("removes expired entries", func(t *testing.T) { - validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"} - expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"} - expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"} + validMeta := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "valid"}} + expiredMeta1 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired1"}} + expiredMeta2 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired2"}} provider := &mockProvider{ getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { diff --git a/apps/plugins/pkg/app/meta/provider.go b/apps/plugins/pkg/app/meta/provider.go index 8b5d8b0fcbd..3c01361d656 100644 --- a/apps/plugins/pkg/app/meta/provider.go +++ b/apps/plugins/pkg/app/meta/provider.go @@ -14,14 +14,14 @@ var ( // Result contains plugin metadata along with its recommended TTL. type Result struct { - Meta pluginsv0alpha1.PluginMetaJSONData + Meta pluginsv0alpha1.MetaSpec TTL time.Duration } // Provider is used for retrieving plugin metadata. type Provider interface { // GetMeta retrieves plugin metadata for the given plugin ID and version. - // Returns the Result containing the PluginMetaJSONData and its recommended TTL. + // Returns the Result containing the MetaJSONData and its recommended TTL. // If the plugin is not found, returns ErrMetaNotFound. GetMeta(ctx context.Context, pluginID, version string) (*Result, error) } diff --git a/apps/plugins/pkg/app/storage.go b/apps/plugins/pkg/app/storage.go index 575b1ca21c8..01e62b9c4ec 100644 --- a/apps/plugins/pkg/app/storage.go +++ b/apps/plugins/pkg/app/storage.go @@ -22,15 +22,15 @@ import ( ) 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) + _ rest.Scoper = (*MetaStorage)(nil) + _ rest.SingularNameProvider = (*MetaStorage)(nil) + _ rest.Getter = (*MetaStorage)(nil) + _ rest.Lister = (*MetaStorage)(nil) + _ rest.Storage = (*MetaStorage)(nil) + _ rest.TableConvertor = (*MetaStorage)(nil) ) -type PluginMetaStorage struct { +type MetaStorage struct { metaManager *meta.ProviderManager client *pluginsv0alpha1.PluginClient clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error) @@ -41,16 +41,16 @@ type PluginMetaStorage struct { tableConverter rest.TableConvertor } -func NewPluginMetaStorage( +func NewMetaStorage( metaManager *meta.ProviderManager, clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error), -) *PluginMetaStorage { +) *MetaStorage { gr := schema.GroupResource{ Group: pluginsv0alpha1.APIGroup, - Resource: strings.ToLower(pluginsv0alpha1.PluginMetaKind().Plural()), + Resource: strings.ToLower(pluginsv0alpha1.MetaKind().Plural()), } - return &PluginMetaStorage{ + return &MetaStorage{ metaManager: metaManager, clientFactory: clientFactory, gr: gr, @@ -58,7 +58,7 @@ func NewPluginMetaStorage( } } -func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { +func (s *MetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { s.clientOnce.Do(func() { client, err := s.clientFactory(ctx) if err != nil { @@ -72,29 +72,29 @@ func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.Plu return s.client, s.clientErr } -func (s *PluginMetaStorage) New() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroValue() +func (s *MetaStorage) New() runtime.Object { + return pluginsv0alpha1.MetaKind().ZeroValue() } -func (s *PluginMetaStorage) Destroy() {} +func (s *MetaStorage) Destroy() {} -func (s *PluginMetaStorage) NamespaceScoped() bool { +func (s *MetaStorage) NamespaceScoped() bool { return true } -func (s *PluginMetaStorage) GetSingularName() string { - return strings.ToLower(pluginsv0alpha1.PluginMetaKind().Kind()) +func (s *MetaStorage) GetSingularName() string { + return strings.ToLower(pluginsv0alpha1.MetaKind().Kind()) } -func (s *PluginMetaStorage) NewList() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroListValue() +func (s *MetaStorage) NewList() runtime.Object { + return pluginsv0alpha1.MetaKind().ZeroListValue() } -func (s *PluginMetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { +func (s *MetaStorage) 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) { +func (s *MetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -111,8 +111,8 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L return nil, apierrors.NewInternalError(fmt.Errorf("failed to list plugins: %w", err)) } - // Convert each Plugin to PluginMeta - metaItems := make([]pluginsv0alpha1.PluginMeta, 0, len(plugins.Items)) + // Convert each Plugin to Meta + metaItems := make([]pluginsv0alpha1.Meta, 0, len(plugins.Items)) for _, plugin := range plugins.Items { result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version) if err != nil { @@ -121,14 +121,25 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L continue } - pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace) - metaItems = append(metaItems, *pluginMeta) + pluginMeta := pluginsv0alpha1.Meta{ + ObjectMeta: metav1.ObjectMeta{ + Name: plugin.Name, + Namespace: plugin.Namespace, + }, + Spec: result.Meta, + } + pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ + Group: pluginsv0alpha1.APIGroup, + Version: pluginsv0alpha1.APIVersion, + Kind: pluginsv0alpha1.MetaKind().Kind(), + }) + metaItems = append(metaItems, pluginMeta) } - list := &pluginsv0alpha1.PluginMetaList{ + list := &pluginsv0alpha1.MetaList{ TypeMeta: metav1.TypeMeta{ APIVersion: pluginsv0alpha1.APIGroup + "/" + pluginsv0alpha1.APIVersion, - Kind: pluginsv0alpha1.PluginMetaKind().Kind() + "List", + Kind: pluginsv0alpha1.MetaKind().Kind() + "List", }, Items: metaItems, } @@ -136,7 +147,7 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L return list, nil } -func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { +func (s *MetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -169,27 +180,18 @@ func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err)) } - return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil -} - -// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata. -func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta { - pluginMeta := &pluginsv0alpha1.PluginMeta{ + pluginMeta := &pluginsv0alpha1.Meta{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: pluginsv0alpha1.PluginMetaSpec{ - PluginJSON: pluginJSON, + Name: plugin.Name, + Namespace: plugin.Namespace, }, + Spec: result.Meta, } - - // Set the GroupVersionKind pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ Group: pluginsv0alpha1.APIGroup, Version: pluginsv0alpha1.APIVersion, - Kind: pluginsv0alpha1.PluginMetaKind().Kind(), + Kind: pluginsv0alpha1.MetaKind().Kind(), }) - return pluginMeta + return pluginMeta, nil } diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 661002a3b59..520609347ae 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,10 +3,10 @@ module github.com/grafana/grafana/apps/preferences go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( @@ -17,19 +17,29 @@ require ( 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-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/grafana/grafana-app-sdk/logging v0.48.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // 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/mailru/easyjson v0.9.1 // 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 @@ -40,29 +50,29 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/client-go v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 75a19848d73..aa5de8cbe99 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -14,29 +14,55 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -58,8 +84,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -76,8 +102,6 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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= @@ -85,10 +109,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -101,16 +125,16 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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/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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -126,24 +150,24 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -154,31 +178,31 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 3cccd7dd8be..62f3f1bf49c 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -7,17 +7,18 @@ 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.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 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.3.0 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/stretchr/testify v1.11.1 - golang.org/x/oauth2 v0.33.0 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/client-go v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + golang.org/x/oauth2 v0.34.0 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/client-go v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) @@ -28,64 +29,81 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // 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/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v64 v64.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.5 // 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/klauspost/compress v1.18.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pkg/errors v0.9.1 // 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.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.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/component-base v0.34.3 // 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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 0e1ba180fad..8b5733639be 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -14,6 +14,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/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.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -23,22 +25,50 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -62,16 +92,21 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= +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= @@ -86,8 +121,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -96,14 +131,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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= @@ -113,10 +156,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -128,6 +171,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= @@ -135,16 +182,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -156,8 +203,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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,17 +217,17 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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= @@ -191,23 +238,23 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -216,48 +263,48 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/provisioning/kinds/connection.cue b/apps/provisioning/kinds/connection.cue new file mode 100644 index 00000000000..23af1991c02 --- /dev/null +++ b/apps/provisioning/kinds/connection.cue @@ -0,0 +1,73 @@ +package repository + +connection: { + kind: "Connection" + pluralName: "Connections" + current: "v0alpha1" + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + schema: { + #GitHubConnectionConfig: { + // App-level information + // GitHub App ID + appID: int + + // Installation-level information + // GitHub App installation ID + installationID: int + } + #BitbucketConnectionConfig: { + // The app clientID + clientID: string + } + #GitlabConnectionConfig: { + // The app clientID + clientID: string + } + #HealthStatus: { + // When not healthy, requests will not be executed + healthy: bool + // When the health was checked last time + checked?: int + // Summary messages (can be shown to users) + // Will only be populated when not healthy + message?: [...string] + } + spec: { + // The connection provider type + type: "github" | "bitbucket" | "gitlab" + // The connection URL + url: *"" | string + // GitHub connection configuration + // Only applicable when provider is "github" + github?: #GitHubConnectionConfig + // Bitbucket connection configuration + // Only applicable when provider is "bitbucket" + bitbucket?: #BitbucketConnectionConfig + // Gitlab connection configuration + // Only applicable when provider is "gitlab" + gitlab?: #GitlabConnectionConfig + } + status: { + // The generation of the spec last time reconciliation ran + observedGeneration?: int + // Connection state + state: "connected" | "disconnected" + // The connection health status + health: #HealthStatus + } + } + } + } +} + diff --git a/apps/provisioning/kinds/manifest.cue b/apps/provisioning/kinds/manifest.cue index 40ffa64d922..b0202cb47ce 100644 --- a/apps/provisioning/kinds/manifest.cue +++ b/apps/provisioning/kinds/manifest.cue @@ -1,9 +1,11 @@ package repository manifest: { - appName: "provisioning" - groupOverride: "provisioning.grafana.app" - kinds: [ + appName: "provisioning" + groupOverride: "provisioning.grafana.app" + preferredVersion: "v0alpha1" + kinds: [ repository, + connection ] -} \ No newline at end of file +} diff --git a/apps/provisioning/kinds/repository.cue b/apps/provisioning/kinds/repository.cue index ba36b4b962f..629d7a62885 100644 --- a/apps/provisioning/kinds/repository.cue +++ b/apps/provisioning/kinds/repository.cue @@ -80,10 +80,13 @@ repository: { // Enabled must be saved as true before any sync job will run enabled: bool // Where values should be saved - target: "unified" | "legacy" + target: "instance" | "folder" // When non-zero, the sync will run periodically intervalSeconds?: int } + #ConnectionInfo: { + name: string + } #HealthStatus: { // When not healthy, requests will not be executed healthy: bool @@ -152,6 +155,9 @@ repository: { // The repository on GitLab. // Mutually exclusive with local | github | git. gitlab?: #GitLabRepositoryConfig + // The connection the repository references. + // This means the Repository is interacting with git via a Connection. + connection?: #ConnectionInfo } status: { // The generation of the spec last time reconciliation ran @@ -168,4 +174,4 @@ repository: { } } } -} \ No newline at end of file +} diff --git a/apps/provisioning/pkg/apis/manifestdata/provisioning_manifest.go b/apps/provisioning/pkg/apis/manifestdata/provisioning_manifest.go new file mode 100644 index 00000000000..c13e0389c11 --- /dev/null +++ b/apps/provisioning/pkg/apis/manifestdata/provisioning_manifest.go @@ -0,0 +1,92 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +import ( + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" +) + +var appManifestData = app.ManifestData{ + AppName: "provisioning", + Group: "provisioning.grafana.app", + PreferredVersion: "v0alpha1", + Versions: []app.ManifestVersion{}, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("provisioning") +} + +var kindVersionToGoType = map[string]resource.Kind{} + +// 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/provisioning/pkg/apis/provisioning/v0alpha1/connections.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/connections.go new file mode 100644 index 00000000000..228523f598e --- /dev/null +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/connections.go @@ -0,0 +1,118 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// When this code is changed, make sure to update the code generation. +// As of writing, this can be done via the hack dir in the root of the repo: ./hack/update-codegen.sh provisioning +// If you've opened the generated files in this dir at some point in VSCode, you may also have to re-open them to clear errors. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Connection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConnectionSpec `json:"spec,omitempty"` + Secure ConnectionSecure `json:"secure,omitzero,omitempty"` + Status ConnectionStatus `json:"status,omitempty"` +} + +type ConnectionSecure struct { + // PrivateKey is the reference to the private key used for GitHub App authentication. + // This value is stored securely and cannot be read back + PrivateKey common.InlineSecureValue `json:"privateKey,omitzero,omitempty"` + + // ClientSecret is the reference to the secret used for other providers authentication, + // and Github on-behalf-of authentication. + // This value is stored securely and cannot be read back + ClientSecret common.InlineSecureValue `json:"clientSecret,omitzero,omitempty"` + + // Token is the reference of the token used to act as the Connection. + // This value is stored securely and cannot be read back + Token common.InlineSecureValue `json:"webhook,omitzero,omitempty"` +} + +func (v ConnectionSecure) IsZero() bool { + return v.PrivateKey.IsZero() && v.Token.IsZero() +} + +type GitHubConnectionConfig struct { + // GitHub App ID + AppID string `json:"appID"` + + // GitHub App installation ID + InstallationID string `json:"installationID"` +} + +type BitbucketConnectionConfig struct { + // App client ID + ClientID string `json:"clientID"` +} + +type GitlabConnectionConfig struct { + // App client ID + ClientID string `json:"clientID"` +} + +// ConnectionType defines the types of Connection providers +// +enum +type ConnectionType string + +// ConnectionType values. +const ( + GithubConnectionType ConnectionType = "github" + GitlabConnectionType ConnectionType = "gitlab" + BitbucketConnectionType ConnectionType = "bitbucket" +) + +type ConnectionSpec struct { + // The connection provider type + Type ConnectionType `json:"type"` + // The connection URL + URL string `json:"url,omitempty"` + + // GitHub connection configuration + // Only applicable when provider is "github" + GitHub *GitHubConnectionConfig `json:"github,omitempty"` + // Bitbucket connection configuration + // Only applicable when provider is "bitbucket" + Bitbucket *BitbucketConnectionConfig `json:"bitbucket,omitempty"` + // Gitlab connection configuration + // Only applicable when provider is "gitlab" + Gitlab *GitlabConnectionConfig `json:"gitlab,omitempty"` +} + +// ConnectionState defines the state of a Connection +// +enum +type ConnectionState string + +// ConnectionState values +const ( + ConnectionStateConnected ConnectionState = "connected" + ConnectionStateDisconnected ConnectionState = "disconnected" +) + +// The status of a Connection. +// This is expected never to be created by a kubectl call or similar, and is expected to rarely (if ever) be edited manually. +type ConnectionStatus struct { + // The generation of the spec last time reconciliation ran + ObservedGeneration int64 `json:"observedGeneration"` + + // Connection state + State ConnectionState `json:"state"` + + // The connection health status + Health HealthStatus `json:"health"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + // +listType=atomic + Items []Connection `json:"items"` +} diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/health.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/health.go new file mode 100644 index 00000000000..1298580c9a9 --- /dev/null +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/health.go @@ -0,0 +1,26 @@ +package v0alpha1 + +// HealthFailureType represents different types of healthcheck failures +// +enum +type HealthFailureType string + +const ( + HealthFailureHook HealthFailureType = "hook" + HealthFailureHealth HealthFailureType = "health" +) + +type HealthStatus struct { + // When not healthy, requests will not be executed + Healthy bool `json:"healthy"` + + // The type of the error + Error HealthFailureType `json:"error,omitempty"` + + // When the health was checked last time + Checked int64 `json:"checked,omitempty"` + + // Summary messages (can be shown to users) + // Will only be populated when not healthy + // +listType=atomic + Message []string `json:"message,omitempty"` +} diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/jobs.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/jobs.go index b96fb1a6d27..100c75b080b 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/jobs.go @@ -136,9 +136,6 @@ type ExportJobOptions struct { } type MigrateJobOptions struct { - // Preserve history (if possible) - History bool `json:"history,omitempty"` - // Message to use when committing the changes in a single commit Message string `json:"message,omitempty"` } @@ -198,6 +195,7 @@ type JobStatus struct { Finished int64 `json:"finished,omitempty"` Message string `json:"message,omitempty"` Errors []string `json:"errors,omitempty"` + Warnings []string `json:"warnings,omitempty"` // Optional value 0-100 that can be set while running Progress float64 `json:"progress,omitempty"` @@ -225,18 +223,20 @@ type JobResourceSummary struct { Kind string `json:"kind,omitempty"` Total int64 `json:"total,omitempty"` // the count (if known) - Create int64 `json:"create,omitempty"` - Update int64 `json:"update,omitempty"` - Delete int64 `json:"delete,omitempty"` - Write int64 `json:"write,omitempty"` // Create or update (export) - Error int64 `json:"error,omitempty"` // The error count + Create int64 `json:"create,omitempty"` + Update int64 `json:"update,omitempty"` + Delete int64 `json:"delete,omitempty"` + Write int64 `json:"write,omitempty"` // Create or update (export) + Error int64 `json:"error,omitempty"` // The error count + Warning int64 `json:"warning,omitempty"` // The warning count // No action required (useful for sync) Noop int64 `json:"noop,omitempty"` - // Report errors for this resource type + // Report errors/warnings for this resource type // This may not be an exhaustive list and recommend looking at the logs for more info - Errors []string `json:"errors,omitempty"` + Errors []string `json:"errors,omitempty"` + Warnings []string `json:"warnings,omitempty"` } // HistoricJob is an append only log, saving all jobs that have been processed. diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/register.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/register.go index 777484828f6..f06798c0ddd 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/register.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/register.go @@ -115,6 +115,47 @@ var HistoricJobResourceInfo = utils.NewResourceInfo(GROUP, VERSION, }, }) +var ConnectionResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "connections", "connection", "Connection", + func() runtime.Object { return &Connection{} }, // newObj + func() runtime.Object { return &ConnectionList{} }, // newList + utils.TableColumns{ // Returned by `kubectl get`. Doesn't affect disk storage. + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + {Name: "Type", Type: "string"}, + {Name: "AppID", Type: "string"}, + {Name: "InstallationID", Type: "string"}, + {Name: "ClientID", Type: "string"}, + }, + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*Connection) + if !ok { + return nil, errors.New("expected Repository") + } + + var appID, installationID, clientID string + switch m.Spec.Type { + case GithubConnectionType: + appID = m.Spec.GitHub.AppID + installationID = m.Spec.GitHub.InstallationID + case BitbucketConnectionType: + clientID = m.Spec.Bitbucket.ClientID + case GitlabConnectionType: + clientID = m.Spec.Gitlab.ClientID + } + + return []interface{}{ + m.Name, + m.CreationTimestamp.UTC().Format(time.RFC3339), + m.Spec.Type, + appID, + installationID, + clientID, + }, nil + }, + }) + var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} @@ -154,6 +195,8 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error { &RefList{}, &HistoricJob{}, &HistoricJobList{}, + &Connection{}, + &ConnectionList{}, ) return nil } diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go index 3031bde77cb..1aab919c9f6 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go @@ -9,11 +9,6 @@ import ( type RepositoryViewList struct { metav1.TypeMeta `json:",inline"` - // The backend is using legacy storage - // FIXME: Not sure where this should be exposed... but we need it somewhere - // The UI should force the onboarding workflow when this is true - LegacyStorage bool `json:"legacyStorage,omitempty"` - // The valid targets (can disable instance or folder types) AllowedTargets []SyncTargetType `json:"allowedTargets,omitempty"` diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go index dff7c3813e5..c2247e21809 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/types.go @@ -219,6 +219,10 @@ func (r *Repository) Path() string { return "" } +type ConnectionInfo struct { + Name string `json:"name"` +} + type RepositorySpec struct { // The repository display name (shown in the UI) Title string `json:"title"` @@ -256,6 +260,10 @@ type RepositorySpec struct { // The repository on GitLab. // Mutually exclusive with local | github | git. GitLab *GitLabRepositoryConfig `json:"gitlab,omitempty"` + + // The connection the repository references. + // This means the Repository is interacting with git via a Connection. + Connection *ConnectionInfo `json:"connection,omitempty"` } // SyncTargetType defines where we want all values to resolve @@ -315,31 +323,6 @@ type RepositoryStatus struct { DeleteError string `json:"deleteError,omitempty"` } -// HealthFailureType represents different types of repository failures -// +enum -type HealthFailureType string - -const ( - HealthFailureHook HealthFailureType = "hook" - HealthFailureHealth HealthFailureType = "health" -) - -type HealthStatus struct { - // When not healthy, requests will not be executed - Healthy bool `json:"healthy"` - - // The type of the error - Error HealthFailureType `json:"error,omitempty"` - - // When the health was checked last time - Checked int64 `json:"checked,omitempty"` - - // Summary messages (can be shown to users) - // Will only be populated when not healthy - // +listType=atomic - Message []string `json:"message,omitempty"` -} - type SyncStatus struct { // pending, running, success, error State JobState `json:"state"` diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 8a3def39e8d..48d03d23f99 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ func (in *Author) DeepCopy() *Author { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BitbucketConnectionConfig) DeepCopyInto(out *BitbucketConnectionConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitbucketConnectionConfig. +func (in *BitbucketConnectionConfig) DeepCopy() *BitbucketConnectionConfig { + if in == nil { + return nil + } + out := new(BitbucketConnectionConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BitbucketRepositoryConfig) DeepCopyInto(out *BitbucketRepositoryConfig) { *out = *in @@ -43,6 +59,151 @@ func (in *BitbucketRepositoryConfig) DeepCopy() *BitbucketRepositoryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Connection) DeepCopyInto(out *Connection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Secure = in.Secure + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Connection. +func (in *Connection) DeepCopy() *Connection { + if in == nil { + return nil + } + out := new(Connection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Connection) 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 *ConnectionInfo) DeepCopyInto(out *ConnectionInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionInfo. +func (in *ConnectionInfo) DeepCopy() *ConnectionInfo { + if in == nil { + return nil + } + out := new(ConnectionInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionList) DeepCopyInto(out *ConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Connection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionList. +func (in *ConnectionList) DeepCopy() *ConnectionList { + if in == nil { + return nil + } + out := new(ConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ConnectionList) 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 *ConnectionSecure) DeepCopyInto(out *ConnectionSecure) { + *out = *in + out.PrivateKey = in.PrivateKey + out.ClientSecret = in.ClientSecret + out.Token = in.Token + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSecure. +func (in *ConnectionSecure) DeepCopy() *ConnectionSecure { + if in == nil { + return nil + } + out := new(ConnectionSecure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { + *out = *in + if in.GitHub != nil { + in, out := &in.GitHub, &out.GitHub + *out = new(GitHubConnectionConfig) + **out = **in + } + if in.Bitbucket != nil { + in, out := &in.Bitbucket, &out.Bitbucket + *out = new(BitbucketConnectionConfig) + **out = **in + } + if in.Gitlab != nil { + in, out := &in.Gitlab, &out.Gitlab + *out = new(GitlabConnectionConfig) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSpec. +func (in *ConnectionSpec) DeepCopy() *ConnectionSpec { + if in == nil { + return nil + } + out := new(ConnectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectionStatus) DeepCopyInto(out *ConnectionStatus) { + *out = *in + in.Health.DeepCopyInto(&out.Health) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionStatus. +func (in *ConnectionStatus) DeepCopy() *ConnectionStatus { + if in == nil { + return nil + } + out := new(ConnectionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeleteJobOptions) DeepCopyInto(out *DeleteJobOptions) { *out = *in @@ -148,6 +309,22 @@ func (in *FileList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubConnectionConfig) DeepCopyInto(out *GitHubConnectionConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubConnectionConfig. +func (in *GitHubConnectionConfig) DeepCopy() *GitHubConnectionConfig { + if in == nil { + return nil + } + out := new(GitHubConnectionConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubRepositoryConfig) DeepCopyInto(out *GitHubRepositoryConfig) { *out = *in @@ -196,6 +373,22 @@ func (in *GitRepositoryConfig) DeepCopy() *GitRepositoryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitlabConnectionConfig) DeepCopyInto(out *GitlabConnectionConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabConnectionConfig. +func (in *GitlabConnectionConfig) DeepCopy() *GitlabConnectionConfig { + if in == nil { + return nil + } + out := new(GitlabConnectionConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HealthStatus) DeepCopyInto(out *HealthStatus) { *out = *in @@ -401,6 +594,11 @@ func (in *JobResourceSummary) DeepCopyInto(out *JobResourceSummary) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Warnings != nil { + in, out := &in.Warnings, &out.Warnings + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -468,6 +666,11 @@ func (in *JobStatus) DeepCopyInto(out *JobStatus) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Warnings != nil { + in, out := &in.Warnings, &out.Warnings + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Summary != nil { in, out := &in.Summary, &out.Summary *out = make([]*JobResourceSummary, len(*in)) @@ -735,6 +938,11 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { *out = new(GitLabRepositoryConfig) **out = **in } + if in.Connection != nil { + in, out := &in.Connection, &out.Connection + *out = new(ConnectionInfo) + **out = **in + } return } diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 9a4a99d703a..a96fa2d8a6b 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -15,15 +15,24 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_BitbucketConnectionConfig(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.Connection": schema_pkg_apis_provisioning_v0alpha1_Connection(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionInfo": schema_pkg_apis_provisioning_v0alpha1_ConnectionInfo(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionList": schema_pkg_apis_provisioning_v0alpha1_ConnectionList(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSecure": schema_pkg_apis_provisioning_v0alpha1_ConnectionSecure(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSpec": schema_pkg_apis_provisioning_v0alpha1_ConnectionSpec(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionStatus": schema_pkg_apis_provisioning_v0alpha1_ConnectionStatus(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.DeleteJobOptions": schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubConnectionConfig(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitLabRepositoryConfig(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref), + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitlabConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_GitlabConnectionConfig(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.HealthStatus": schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.HistoricJob": schema_pkg_apis_provisioning_v0alpha1_HistoricJob(ref), "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.HistoricJobList": schema_pkg_apis_provisioning_v0alpha1_HistoricJobList(ref), @@ -100,6 +109,27 @@ func schema_pkg_apis_provisioning_v0alpha1_Author(ref common.ReferenceCallback) } } +func schema_pkg_apis_provisioning_v0alpha1_BitbucketConnectionConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "clientID": { + SchemaProps: spec.SchemaProps{ + Description: "App client ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"clientID"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -142,6 +172,256 @@ func schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref common. } } +func schema_pkg_apis_provisioning_v0alpha1_Connection(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "When this code is changed, make sure to update the code generation. As of writing, this can be done via the hack dir in the root of the repo: ./hack/update-codegen.sh provisioning If you've opened the generated files in this dir at some point in VSCode, you may also have to re-open them to clear errors.", + 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{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSpec"), + }, + }, + "secure": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSecure"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSecure", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionSpec", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_ConnectionInfo(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: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_ConnectionList(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": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + 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/provisioning/pkg/apis/provisioning/v0alpha1.Connection"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.Connection", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_ConnectionSecure(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "privateKey": { + SchemaProps: spec.SchemaProps{ + Description: "PrivateKey is the reference to the private key used for GitHub App authentication. This value is stored securely and cannot be read back", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"), + }, + }, + "clientSecret": { + SchemaProps: spec.SchemaProps{ + Description: "ClientSecret is the reference to the secret used for other providers authentication, and Github on-behalf-of authentication. This value is stored securely and cannot be read back", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"), + }, + }, + "webhook": { + SchemaProps: spec.SchemaProps{ + Description: "Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"}, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_ConnectionSpec(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{ + Description: "The connection provider type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"github\"`\n - `\"gitlab\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"bitbucket", "github", "gitlab"}, + }, + }, + "url": { + SchemaProps: spec.SchemaProps{ + Description: "The connection URL", + Type: []string{"string"}, + Format: "", + }, + }, + "github": { + SchemaProps: spec.SchemaProps{ + Description: "GitHub connection configuration Only applicable when provider is \"github\"", + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig"), + }, + }, + "bitbucket": { + SchemaProps: spec.SchemaProps{ + Description: "Bitbucket connection configuration Only applicable when provider is \"bitbucket\"", + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketConnectionConfig"), + }, + }, + "gitlab": { + SchemaProps: spec.SchemaProps{ + Description: "Gitlab connection configuration Only applicable when provider is \"gitlab\"", + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitlabConnectionConfig"), + }, + }, + }, + Required: []string{"type"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketConnectionConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitlabConnectionConfig"}, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_ConnectionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "The status of a Connection. This is expected never to be created by a kubectl call or similar, and is expected to rarely (if ever) be edited manually.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "The generation of the spec last time reconciliation ran", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "Connection state\n\nPossible enum values:\n - `\"connected\"`\n - `\"disconnected\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"connected", "disconnected"}, + }, + }, + "health": { + SchemaProps: spec.SchemaProps{ + Description: "The connection health status", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.HealthStatus"), + }, + }, + }, + Required: []string{"observedGeneration", "state", "health"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.HealthStatus"}, + } +} + func schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -362,6 +642,35 @@ func schema_pkg_apis_provisioning_v0alpha1_FileList(ref common.ReferenceCallback } } +func schema_pkg_apis_provisioning_v0alpha1_GitHubConnectionConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "appID": { + SchemaProps: spec.SchemaProps{ + Description: "GitHub App ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "installationID": { + SchemaProps: spec.SchemaProps{ + Description: "GitHub App installation ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"appID", "installationID"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -481,6 +790,27 @@ func schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref common.Refere } } +func schema_pkg_apis_provisioning_v0alpha1_GitlabConnectionConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "clientID": { + SchemaProps: spec.SchemaProps{ + Description: "App client ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"clientID"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -889,6 +1219,13 @@ func schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref common.Referen Format: "int64", }, }, + "warning": { + SchemaProps: spec.SchemaProps{ + Description: "The error count", + Type: []string{"integer"}, + Format: "int64", + }, + }, "noop": { SchemaProps: spec.SchemaProps{ Description: "No action required (useful for sync)", @@ -898,7 +1235,7 @@ func schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref common.Referen }, "errors": { SchemaProps: spec.SchemaProps{ - Description: "Report errors for this resource type This may not be an exhaustive list and recommend looking at the logs for more info", + Description: "Report errors/warnings for this resource type This may not be an exhaustive list and recommend looking at the logs for more info", Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ @@ -911,6 +1248,20 @@ func schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref common.Referen }, }, }, + "warnings": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, }, }, @@ -1029,6 +1380,20 @@ func schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref common.ReferenceCallbac }, }, }, + "warnings": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, "progress": { SchemaProps: spec.SchemaProps{ Description: "Optional value 0-100 that can be set while running", @@ -1130,13 +1495,6 @@ func schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref common.Referenc SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "history": { - SchemaProps: spec.SchemaProps{ - Description: "Preserve history (if possible)", - Type: []string{"boolean"}, - Format: "", - }, - }, "message": { SchemaProps: spec.SchemaProps{ Description: "Message to use when committing the changes in a single commit", @@ -1524,12 +1882,18 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCa Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig"), }, }, + "connection": { + SchemaProps: spec.SchemaProps{ + Description: "The connection the repository references. This means the Repository is interacting with git via a Connection.", + Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionInfo"), + }, + }, }, Required: []string{"title", "workflows", "sync", "type"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.SyncOptions"}, + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ConnectionInfo", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig", "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.SyncOptions"}, } } @@ -1748,13 +2112,6 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref common.Referen Format: "", }, }, - "legacyStorage": { - SchemaProps: spec.SchemaProps{ - Description: "The backend is using legacy storage FIXME: Not sure where this should be exposed... but we need it somewhere The UI should force the onboarding workflow when this is true", - Type: []string{"boolean"}, - Format: "", - }, - }, "allowedTargets": { SchemaProps: spec.SchemaProps{ Description: "The valid targets (can disable instance or folder types)", diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index c67b1462a9e..b9504855b80 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,10 +1,13 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionList,Items API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Paths API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Resources API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,FileList,Items API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,HistoryList,Items API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors +API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Warnings API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobStatus,Errors API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobStatus,Summary +API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobStatus,Warnings API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ManagerStats,Stats API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,MoveJobOptions,Paths API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,MoveJobOptions,Resources @@ -18,6 +21,8 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioni API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ResourceList,Items API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,TestResults,Errors API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,WebhookStatus,SubscribedEvents +API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionSecure,Token +API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionSpec,GitHub API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobSpec,PullRequest API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobStatus,URLs API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ManagerStats,Identity diff --git a/apps/provisioning/pkg/auth/access_checker.go b/apps/provisioning/pkg/auth/access_checker.go new file mode 100644 index 00000000000..841c5421f2a --- /dev/null +++ b/apps/provisioning/pkg/auth/access_checker.go @@ -0,0 +1,22 @@ +package auth + +import ( + "context" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +//go:generate mockery --name AccessChecker --structname MockAccessChecker --inpackage --filename access_checker_mock.go --with-expecter + +// AccessChecker provides access control checks with optional role-based fallback. +type AccessChecker interface { + // Check performs an access check and returns nil if allowed, or an appropriate + // API error if denied. If req.Namespace is empty, it will be filled from the + // identity's namespace. + Check(ctx context.Context, req authlib.CheckRequest, folder string) error + + // WithFallbackRole returns an AccessChecker configured with the specified fallback role. + // Whether the fallback is actually applied depends on the implementation. + WithFallbackRole(role identity.RoleType) AccessChecker +} diff --git a/apps/provisioning/pkg/auth/access_checker_mock.go b/apps/provisioning/pkg/auth/access_checker_mock.go new file mode 100644 index 00000000000..d0f1cddd7fc --- /dev/null +++ b/apps/provisioning/pkg/auth/access_checker_mock.go @@ -0,0 +1,135 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package auth + +import ( + context "context" + + identity "github.com/grafana/grafana/pkg/apimachinery/identity" + mock "github.com/stretchr/testify/mock" + + types "github.com/grafana/authlib/types" +) + +// MockAccessChecker is an autogenerated mock type for the AccessChecker type +type MockAccessChecker struct { + mock.Mock +} + +type MockAccessChecker_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAccessChecker) EXPECT() *MockAccessChecker_Expecter { + return &MockAccessChecker_Expecter{mock: &_m.Mock} +} + +// Check provides a mock function with given fields: ctx, req, folder +func (_m *MockAccessChecker) Check(ctx context.Context, req types.CheckRequest, folder string) error { + ret := _m.Called(ctx, req, folder) + + if len(ret) == 0 { + panic("no return value specified for Check") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.CheckRequest, string) error); ok { + r0 = rf(ctx, req, folder) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockAccessChecker_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check' +type MockAccessChecker_Check_Call struct { + *mock.Call +} + +// Check is a helper method to define mock.On call +// - ctx context.Context +// - req types.CheckRequest +// - folder string +func (_e *MockAccessChecker_Expecter) Check(ctx interface{}, req interface{}, folder interface{}) *MockAccessChecker_Check_Call { + return &MockAccessChecker_Check_Call{Call: _e.mock.On("Check", ctx, req, folder)} +} + +func (_c *MockAccessChecker_Check_Call) Run(run func(ctx context.Context, req types.CheckRequest, folder string)) *MockAccessChecker_Check_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(types.CheckRequest), args[2].(string)) + }) + return _c +} + +func (_c *MockAccessChecker_Check_Call) Return(_a0 error) *MockAccessChecker_Check_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockAccessChecker_Check_Call) RunAndReturn(run func(context.Context, types.CheckRequest, string) error) *MockAccessChecker_Check_Call { + _c.Call.Return(run) + return _c +} + +// WithFallbackRole provides a mock function with given fields: role +func (_m *MockAccessChecker) WithFallbackRole(role identity.RoleType) AccessChecker { + ret := _m.Called(role) + + if len(ret) == 0 { + panic("no return value specified for WithFallbackRole") + } + + var r0 AccessChecker + if rf, ok := ret.Get(0).(func(identity.RoleType) AccessChecker); ok { + r0 = rf(role) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(AccessChecker) + } + } + + return r0 +} + +// MockAccessChecker_WithFallbackRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithFallbackRole' +type MockAccessChecker_WithFallbackRole_Call struct { + *mock.Call +} + +// WithFallbackRole is a helper method to define mock.On call +// - role identity.RoleType +func (_e *MockAccessChecker_Expecter) WithFallbackRole(role interface{}) *MockAccessChecker_WithFallbackRole_Call { + return &MockAccessChecker_WithFallbackRole_Call{Call: _e.mock.On("WithFallbackRole", role)} +} + +func (_c *MockAccessChecker_WithFallbackRole_Call) Run(run func(role identity.RoleType)) *MockAccessChecker_WithFallbackRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(identity.RoleType)) + }) + return _c +} + +func (_c *MockAccessChecker_WithFallbackRole_Call) Return(_a0 AccessChecker) *MockAccessChecker_WithFallbackRole_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockAccessChecker_WithFallbackRole_Call) RunAndReturn(run func(identity.RoleType) AccessChecker) *MockAccessChecker_WithFallbackRole_Call { + _c.Call.Return(run) + return _c +} + +// NewMockAccessChecker creates a new instance of MockAccessChecker. 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 NewMockAccessChecker(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAccessChecker { + mock := &MockAccessChecker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/apps/provisioning/pkg/auth/round_tripper.go b/apps/provisioning/pkg/auth/round_tripper.go index 0d2f1cb4ac4..f5da0d778f0 100644 --- a/apps/provisioning/pkg/auth/round_tripper.go +++ b/apps/provisioning/pkg/auth/round_tripper.go @@ -1,3 +1,4 @@ +// Package auth provides authentication utilities for the provisioning API. package auth import ( @@ -6,7 +7,6 @@ import ( "net/http" "github.com/grafana/authlib/authn" - "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" utilnet "k8s.io/apimachinery/pkg/util/net" ) @@ -15,29 +15,61 @@ type tokenExchanger interface { Exchange(ctx context.Context, req authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error) } -// RoundTripper injects an exchanged access token for the provisioning API into outgoing requests. -type RoundTripper struct { - client tokenExchanger - transport http.RoundTripper - audience string +// RoundTripperOption configures optional behavior for the RoundTripper. +type RoundTripperOption func(*RoundTripper) + +// ExtraAudience appends an additional audience to the token exchange request. +// +// This is primarily used by operators connecting to the multitenant aggregator, +// where the token must include both the target API server's audience (e.g., dashboards, +// folders) and the provisioning group audience. The provisioning group audience is +// required so that the token passes the enforceManagerProperties check, which prevents +// unauthorized updates to provisioned resources. +// +// Example: +// +// authrt.NewRoundTripper(client, rt, "dashboards.grafana.app", authrt.ExtraAudience("provisioning.grafana.app")) +func ExtraAudience(audience string) RoundTripperOption { + return func(rt *RoundTripper) { + rt.extraAudience = audience + } } -// NewRoundTripper constructs a RoundTripper that exchanges the provided token per request -// and forwards the request to the provided base transport. -func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string) *RoundTripper { - return &RoundTripper{ +// RoundTripper is an http.RoundTripper that performs token exchange before each request. +// It exchanges the service's credentials for an access token scoped to the configured +// audience(s), then injects that token into the outgoing request's X-Access-Token header. +type RoundTripper struct { + client tokenExchanger + transport http.RoundTripper + audience string + extraAudience string +} + +// NewRoundTripper creates a RoundTripper that exchanges tokens for each outgoing request. +// +// Parameters: +// - tokenExchangeClient: the client used to exchange credentials for access tokens +// - base: the underlying transport to delegate requests to after token injection +// - audience: the primary audience for the token (typically the target API server's group) +// - opts: optional configuration (e.g., ExtraAudience to include additional audiences) +func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string, opts ...RoundTripperOption) *RoundTripper { + rt := &RoundTripper{ client: tokenExchangeClient, transport: base, audience: audience, } + for _, opt := range opts { + opt(rt) + } + return rt } +// RoundTrip exchanges credentials for an access token and injects it into the request. +// The token is scoped to all configured audiences and the wildcard namespace ("*"). func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - // when we want to write resources with the provisioning API, the audience needs to include provisioning - // so that it passes the check in enforceManagerProperties, which prevents others from updating provisioned resources audiences := []string{t.audience} - if t.audience != v0alpha1.GROUP { - audiences = append(audiences, v0alpha1.GROUP) + if t.extraAudience != "" && t.extraAudience != t.audience { + audiences = append(audiences, t.extraAudience) } tokenResponse, err := t.client.Exchange(req.Context(), authn.TokenExchangeRequest{ diff --git a/apps/provisioning/pkg/auth/round_tripper_test.go b/apps/provisioning/pkg/auth/round_tripper_test.go index e3ae4b7b3d4..c1b2b81e17f 100644 --- a/apps/provisioning/pkg/auth/round_tripper_test.go +++ b/apps/provisioning/pkg/auth/round_tripper_test.go @@ -71,16 +71,29 @@ func TestRoundTripper_AudiencesAndNamespace(t *testing.T) { tests := []struct { name string audience string + extraAudience string wantAudiences []string }{ { - name: "adds group when custom audience", + name: "uses only provided audience by default", audience: "example-audience", + wantAudiences: []string{"example-audience"}, + }, + { + name: "uses only group audience by default", + audience: v0alpha1.GROUP, + wantAudiences: []string{v0alpha1.GROUP}, + }, + { + name: "extra audience adds provisioning group", + audience: "example-audience", + extraAudience: v0alpha1.GROUP, wantAudiences: []string{"example-audience", v0alpha1.GROUP}, }, { - name: "no duplicate when group audience", + name: "extra audience no duplicate when same as primary", audience: v0alpha1.GROUP, + extraAudience: v0alpha1.GROUP, wantAudiences: []string{v0alpha1.GROUP}, }, } @@ -88,11 +101,15 @@ func TestRoundTripper_AudiencesAndNamespace(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fx := &fakeExchanger{resp: &authn.TokenExchangeResponse{Token: "abc123"}} + var opts []RoundTripperOption + if tt.extraAudience != "" { + opts = append(opts, ExtraAudience(tt.extraAudience)) + } tr := NewRoundTripper(fx, roundTripperFunc(func(_ *http.Request) (*http.Response, error) { rr := httptest.NewRecorder() rr.WriteHeader(http.StatusOK) return rr.Result(), nil - }), tt.audience) + }), tt.audience, opts...) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil) resp, err := tr.RoundTrip(req) diff --git a/apps/provisioning/pkg/auth/session_access_checker.go b/apps/provisioning/pkg/auth/session_access_checker.go new file mode 100644 index 00000000000..1bc6a1b7218 --- /dev/null +++ b/apps/provisioning/pkg/auth/session_access_checker.go @@ -0,0 +1,153 @@ +package auth + +import ( + "context" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// sessionAccessChecker implements AccessChecker using Grafana session identity. +type sessionAccessChecker struct { + inner authlib.AccessChecker + fallbackRole identity.RoleType +} + +// NewSessionAccessChecker creates an AccessChecker that gets identity from Grafana +// sessions via GetRequester(ctx). Supports optional role-based fallback via +// WithFallbackRole for backwards compatibility. +func NewSessionAccessChecker(inner authlib.AccessChecker) AccessChecker { + return &sessionAccessChecker{ + inner: inner, + fallbackRole: "", + } +} + +// WithFallbackRole returns a new AccessChecker with the specified fallback role. +func (c *sessionAccessChecker) WithFallbackRole(role identity.RoleType) AccessChecker { + return &sessionAccessChecker{ + inner: c.inner, + fallbackRole: role, + } +} + +// Check performs an access check with optional role-based fallback. +// Returns nil if access is allowed, or an appropriate API error if denied. +func (c *sessionAccessChecker) Check(ctx context.Context, req authlib.CheckRequest, folder string) error { + logger := logging.FromContext(ctx).With("logger", "sessionAccessChecker") + + // Get identity from Grafana session + requester, err := identity.GetRequester(ctx) + if err != nil { + logger.Debug("failed to get requester", + "resource", req.Resource, + "verb", req.Verb, + "error", err.Error(), + ) + return apierrors.NewUnauthorized(fmt.Sprintf("failed to get requester: %v", err)) + } + + logger.Debug("checking access", + "identityType", requester.GetIdentityType(), + "orgRole", requester.GetOrgRole(), + "namespace", requester.GetNamespace(), + "resource", req.Resource, + "verb", req.Verb, + "group", req.Group, + "name", req.Name, + "folder", folder, + "fallbackRole", c.fallbackRole, + ) + + // Fill in namespace from identity if not provided + if req.Namespace == "" { + req.Namespace = requester.GetNamespace() + } + + // Perform the access check + rsp, err := c.inner.Check(ctx, requester, req, folder) + + // Build the GroupResource for error messages + gr := schema.GroupResource{Group: req.Group, Resource: req.Resource} + + // No fallback configured, return result directly + if c.fallbackRole == "" { + if err != nil { + logger.Debug("access check error (no fallback)", + "resource", req.Resource, + "verb", req.Verb, + "error", err.Error(), + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err)) + } + if !rsp.Allowed { + logger.Debug("access check denied (no fallback)", + "resource", req.Resource, + "verb", req.Verb, + "group", req.Group, + "allowed", rsp.Allowed, + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("permission denied")) + } + logger.Debug("access allowed", + "resource", req.Resource, + "verb", req.Verb, + ) + return nil + } + + // Fallback is configured - apply fallback logic + if err != nil { + if requester.GetOrgRole().Includes(c.fallbackRole) { + logger.Debug("access allowed via role fallback (after error)", + "resource", req.Resource, + "verb", req.Verb, + "fallbackRole", c.fallbackRole, + "orgRole", requester.GetOrgRole(), + ) + return nil // Fallback succeeded + } + logger.Debug("access check error (fallback failed)", + "resource", req.Resource, + "verb", req.Verb, + "error", err.Error(), + "fallbackRole", c.fallbackRole, + "orgRole", requester.GetOrgRole(), + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err)) + } + + if rsp.Allowed { + logger.Debug("access allowed", + "resource", req.Resource, + "verb", req.Verb, + ) + return nil + } + + // Fall back to role for backwards compatibility + if requester.GetOrgRole().Includes(c.fallbackRole) { + logger.Debug("access allowed via role fallback", + "resource", req.Resource, + "verb", req.Verb, + "fallbackRole", c.fallbackRole, + "orgRole", requester.GetOrgRole(), + ) + return nil // Fallback succeeded + } + + logger.Debug("access denied (fallback role not met)", + "resource", req.Resource, + "verb", req.Verb, + "group", req.Group, + "fallbackRole", c.fallbackRole, + "orgRole", requester.GetOrgRole(), + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s role is required", strings.ToLower(string(c.fallbackRole)))) +} diff --git a/apps/provisioning/pkg/auth/session_access_checker_test.go b/apps/provisioning/pkg/auth/session_access_checker_test.go new file mode 100644 index 00000000000..1e99a6e46db --- /dev/null +++ b/apps/provisioning/pkg/auth/session_access_checker_test.go @@ -0,0 +1,244 @@ +package auth + +import ( + "context" + "errors" + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockRequester implements identity.Requester for testing. +type mockRequester struct { + identity.Requester + orgRole identity.RoleType + identityType authlib.IdentityType + namespace string +} + +func (m *mockRequester) GetOrgRole() identity.RoleType { + return m.orgRole +} + +func (m *mockRequester) GetIdentityType() authlib.IdentityType { + return m.identityType +} + +func (m *mockRequester) GetNamespace() string { + return m.namespace +} + +func TestSessionAccessChecker_Check(t *testing.T) { + ctx := context.Background() + req := authlib.CheckRequest{ + Verb: "get", + Group: "provisioning.grafana.app", + Resource: "repositories", + Name: "test-repo", + Namespace: "default", + } + + tests := []struct { + name string + fallbackRole identity.RoleType + innerResponse authlib.CheckResponse + innerErr error + requester *mockRequester + expectAllow bool + }{ + { + name: "allowed by checker", + fallbackRole: identity.RoleAdmin, + innerResponse: authlib.CheckResponse{Allowed: true}, + requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser}, + expectAllow: true, + }, + { + name: "denied by checker, fallback to admin role succeeds", + fallbackRole: identity.RoleAdmin, + innerResponse: authlib.CheckResponse{Allowed: false}, + requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser}, + expectAllow: true, + }, + { + name: "denied by checker, fallback to admin role fails for viewer", + fallbackRole: identity.RoleAdmin, + innerResponse: authlib.CheckResponse{Allowed: false}, + requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser}, + expectAllow: false, + }, + { + name: "error from checker, fallback to admin role succeeds", + fallbackRole: identity.RoleAdmin, + innerErr: errors.New("access check failed"), + requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser}, + expectAllow: true, + }, + { + name: "error from checker, fallback fails for viewer", + fallbackRole: identity.RoleAdmin, + innerErr: errors.New("access check failed"), + requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser}, + expectAllow: false, + }, + { + name: "denied, editor fallback succeeds for editor", + fallbackRole: identity.RoleEditor, + innerResponse: authlib.CheckResponse{Allowed: false}, + requester: &mockRequester{orgRole: identity.RoleEditor, identityType: authlib.TypeUser}, + expectAllow: true, + }, + { + name: "denied, editor fallback fails for viewer", + fallbackRole: identity.RoleEditor, + innerResponse: authlib.CheckResponse{Allowed: false}, + requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser}, + expectAllow: false, + }, + { + name: "no fallback configured, denied stays denied", + fallbackRole: "", // no fallback + innerResponse: authlib.CheckResponse{Allowed: false}, + requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser}, + expectAllow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: tt.innerResponse, + err: tt.innerErr, + } + + checker := NewSessionAccessChecker(mock) + if tt.fallbackRole != "" { + checker = checker.WithFallbackRole(tt.fallbackRole) + } + + // Add requester to context + testCtx := identity.WithRequester(ctx, tt.requester) + + err := checker.Check(testCtx, req, "") + + if tt.expectAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.True(t, apierrors.IsForbidden(err), "expected Forbidden error, got: %v", err) + } + }) + } +} + +func TestSessionAccessChecker_NoRequester(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: true}, + } + + checker := NewSessionAccessChecker(mock) + err := checker.Check(context.Background(), authlib.CheckRequest{}, "") + + require.Error(t, err) + assert.True(t, apierrors.IsUnauthorized(err), "expected Unauthorized error") +} + +func TestSessionAccessChecker_WithFallbackRole_ImmutableOriginal(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: false}, + } + + original := NewSessionAccessChecker(mock) + withAdmin := original.WithFallbackRole(identity.RoleAdmin) + withEditor := original.WithFallbackRole(identity.RoleEditor) + + ctx := identity.WithRequester(context.Background(), &mockRequester{ + orgRole: identity.RoleEditor, + identityType: authlib.TypeUser, + }) + + req := authlib.CheckRequest{} + + // Original should deny (no fallback) + err := original.Check(ctx, req, "") + require.Error(t, err, "original should deny without fallback") + + // WithAdmin should deny for editor + err = withAdmin.Check(ctx, req, "") + require.Error(t, err, "admin fallback should deny for editor") + + // WithEditor should allow for editor + err = withEditor.Check(ctx, req, "") + require.NoError(t, err, "editor fallback should allow for editor") +} + +func TestSessionAccessChecker_WithFallbackRole_ChainedCalls(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: false}, + } + + // Ensure chained WithFallbackRole calls work correctly + checker := NewSessionAccessChecker(mock). + WithFallbackRole(identity.RoleAdmin). + WithFallbackRole(identity.RoleEditor) // This should override admin + + ctx := identity.WithRequester(context.Background(), &mockRequester{ + orgRole: identity.RoleEditor, + identityType: authlib.TypeUser, + }) + + err := checker.Check(ctx, authlib.CheckRequest{}, "") + require.NoError(t, err, "last fallback (editor) should be used") +} + +func TestSessionAccessChecker_RealSignedInUser(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: false}, + } + + checker := NewSessionAccessChecker(mock).WithFallbackRole(identity.RoleAdmin) + + // Use a real SignedInUser + signedInUser := &user.SignedInUser{ + UserID: 1, + OrgID: 1, + OrgRole: identity.RoleAdmin, + } + + ctx := identity.WithRequester(context.Background(), signedInUser) + + err := checker.Check(ctx, authlib.CheckRequest{}, "") + require.NoError(t, err, "admin user should be allowed via fallback") +} + +func TestSessionAccessChecker_FillsNamespace(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: true}, + } + + checker := NewSessionAccessChecker(mock) + + ctx := identity.WithRequester(context.Background(), &mockRequester{ + orgRole: identity.RoleAdmin, + identityType: authlib.TypeUser, + namespace: "org-123", + }) + + // Request without namespace + req := authlib.CheckRequest{ + Verb: "get", + Group: "provisioning.grafana.app", + Resource: "repositories", + Name: "test-repo", + // Namespace intentionally empty + } + + err := checker.Check(ctx, req, "") + require.NoError(t, err) +} diff --git a/apps/provisioning/pkg/auth/token_access_checker.go b/apps/provisioning/pkg/auth/token_access_checker.go new file mode 100644 index 00000000000..8df833d7a34 --- /dev/null +++ b/apps/provisioning/pkg/auth/token_access_checker.go @@ -0,0 +1,92 @@ +package auth + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// tokenAccessChecker implements AccessChecker using access tokens from context. +type tokenAccessChecker struct { + inner authlib.AccessChecker +} + +// NewTokenAccessChecker creates an AccessChecker that gets identity from access tokens +// via AuthInfoFrom(ctx). Role-based fallback is not supported. +func NewTokenAccessChecker(inner authlib.AccessChecker) AccessChecker { + return &tokenAccessChecker{inner: inner} +} + +// WithFallbackRole returns the same checker since fallback is not supported. +func (c *tokenAccessChecker) WithFallbackRole(_ identity.RoleType) AccessChecker { + return c +} + +// Check performs an access check using AuthInfo from context. +// Returns nil if access is allowed, or an appropriate API error if denied. +func (c *tokenAccessChecker) Check(ctx context.Context, req authlib.CheckRequest, folder string) error { + logger := logging.FromContext(ctx).With("logger", "tokenAccessChecker") + + // Get identity from access token in context + id, ok := authlib.AuthInfoFrom(ctx) + if !ok { + logger.Debug("no auth info in context", + "resource", req.Resource, + "verb", req.Verb, + "namespace", req.Namespace, + ) + return apierrors.NewUnauthorized("no auth info in context") + } + + logger.Debug("checking access", + "identityType", id.GetIdentityType(), + "namespace", id.GetNamespace(), + "resource", req.Resource, + "verb", req.Verb, + "group", req.Group, + "name", req.Name, + "folder", folder, + ) + + // Fill in namespace from identity if not provided + if req.Namespace == "" { + req.Namespace = id.GetNamespace() + } + + // Perform the access check + rsp, err := c.inner.Check(ctx, id, req, folder) + + // Build the GroupResource for error messages + gr := schema.GroupResource{Group: req.Group, Resource: req.Resource} + + if err != nil { + logger.Debug("access check error", + "resource", req.Resource, + "verb", req.Verb, + "error", err.Error(), + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err)) + } + if !rsp.Allowed { + logger.Debug("access check denied", + "resource", req.Resource, + "verb", req.Verb, + "group", req.Group, + "identityType", id.GetIdentityType(), + "allowed", rsp.Allowed, + ) + return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("permission denied")) + } + + logger.Debug("access allowed", + "resource", req.Resource, + "verb", req.Verb, + ) + return nil +} diff --git a/apps/provisioning/pkg/auth/token_access_checker_test.go b/apps/provisioning/pkg/auth/token_access_checker_test.go new file mode 100644 index 00000000000..bce0d3a77a0 --- /dev/null +++ b/apps/provisioning/pkg/auth/token_access_checker_test.go @@ -0,0 +1,137 @@ +package auth + +import ( + "context" + "errors" + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTokenAccessChecker_Check(t *testing.T) { + req := authlib.CheckRequest{ + Verb: "get", + Group: "provisioning.grafana.app", + Resource: "repositories", + Name: "test-repo", + Namespace: "default", + } + + tests := []struct { + name string + innerResponse authlib.CheckResponse + innerErr error + authInfo *identity.StaticRequester + expectAllow bool + }{ + { + name: "allowed by checker", + innerResponse: authlib.CheckResponse{Allowed: true}, + authInfo: &identity.StaticRequester{Type: authlib.TypeUser}, + expectAllow: true, + }, + { + name: "denied by checker", + innerResponse: authlib.CheckResponse{Allowed: false}, + authInfo: &identity.StaticRequester{Type: authlib.TypeUser}, + expectAllow: false, + }, + { + name: "error from checker", + innerErr: errors.New("access check failed"), + authInfo: &identity.StaticRequester{Type: authlib.TypeUser}, + expectAllow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: tt.innerResponse, + err: tt.innerErr, + } + + checker := NewTokenAccessChecker(mock) + + // Add auth info to context + testCtx := authlib.WithAuthInfo(context.Background(), tt.authInfo) + + err := checker.Check(testCtx, req, "") + + if tt.expectAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.True(t, apierrors.IsForbidden(err), "expected Forbidden error, got: %v", err) + } + }) + } +} + +func TestTokenAccessChecker_NoAuthInfo(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: true}, + } + + checker := NewTokenAccessChecker(mock) + err := checker.Check(context.Background(), authlib.CheckRequest{}, "") + + require.Error(t, err) + assert.True(t, apierrors.IsUnauthorized(err), "expected Unauthorized error") +} + +func TestTokenAccessChecker_WithFallbackRole_IsNoOp(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: false}, + } + + checker := NewTokenAccessChecker(mock) + checkerWithFallback := checker.WithFallbackRole(identity.RoleAdmin) + + // They should be the same instance + assert.Same(t, checker, checkerWithFallback, "WithFallbackRole should return same instance") +} + +func TestTokenAccessChecker_FillsNamespace(t *testing.T) { + mock := &mockInnerAccessChecker{ + response: authlib.CheckResponse{Allowed: true}, + } + + checker := NewTokenAccessChecker(mock) + + ctx := authlib.WithAuthInfo(context.Background(), &identity.StaticRequester{ + Type: authlib.TypeUser, + Namespace: "org-123", + }) + + // Request without namespace + req := authlib.CheckRequest{ + Verb: "get", + Group: "provisioning.grafana.app", + Resource: "repositories", + Name: "test-repo", + // Namespace intentionally empty + } + + err := checker.Check(ctx, req, "") + require.NoError(t, err) +} + +// mockInnerAccessChecker implements authlib.AccessChecker for testing. +type mockInnerAccessChecker struct { + response authlib.CheckResponse + err error +} + +func (m *mockInnerAccessChecker) Check(_ context.Context, _ authlib.AuthInfo, _ authlib.CheckRequest, _ string) (authlib.CheckResponse, error) { + return m.response, m.err +} + +func (m *mockInnerAccessChecker) Compile(_ context.Context, _ authlib.AuthInfo, _ authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) { + return nil, nil, nil +} diff --git a/apps/provisioning/pkg/connection/mutator.go b/apps/provisioning/pkg/connection/mutator.go new file mode 100644 index 00000000000..30291669905 --- /dev/null +++ b/apps/provisioning/pkg/connection/mutator.go @@ -0,0 +1,28 @@ +package connection + +import ( + "fmt" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +const ( + githubInstallationURL = "https://github.com/settings/installations" +) + +func MutateConnection(connection *provisioning.Connection) error { + switch connection.Spec.Type { + case provisioning.GithubConnectionType: + // Do nothing in case spec.Github is nil. + // If this field is required, we should fail at validation time. + if connection.Spec.GitHub == nil { + return nil + } + + connection.Spec.URL = fmt.Sprintf("%s/%s", githubInstallationURL, connection.Spec.GitHub.InstallationID) + return nil + default: + // TODO: we need to setup the URL for bitbucket and gitlab. + return nil + } +} diff --git a/apps/provisioning/pkg/connection/mutator_test.go b/apps/provisioning/pkg/connection/mutator_test.go new file mode 100644 index 00000000000..a25aabd10a1 --- /dev/null +++ b/apps/provisioning/pkg/connection/mutator_test.go @@ -0,0 +1,35 @@ +package connection_test + +import ( + "testing" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/connection" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMutateConnection(t *testing.T) { + t.Run("should add URL to Github connection", func(t *testing.T) { + c := &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GithubConnectionType, + GitHub: &provisioning.GitHubConnectionConfig{ + AppID: "123", + InstallationID: "456", + }, + }, + Secure: provisioning.ConnectionSecure{ + PrivateKey: common.InlineSecureValue{ + Name: "test-private-key", + }, + }, + } + + require.NoError(t, connection.MutateConnection(c)) + assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL) + }) +} diff --git a/apps/provisioning/pkg/connection/validator.go b/apps/provisioning/pkg/connection/validator.go new file mode 100644 index 00000000000..c2537e3af2f --- /dev/null +++ b/apps/provisioning/pkg/connection/validator.go @@ -0,0 +1,104 @@ +package connection + +import ( + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateConnection(connection *provisioning.Connection) error { + list := field.ErrorList{} + + if connection.Spec.Type == "" { + list = append(list, field.Required(field.NewPath("spec", "type"), "type must be specified")) + } + + switch connection.Spec.Type { + case provisioning.GithubConnectionType: + list = append(list, validateGithubConnection(connection)...) + case provisioning.BitbucketConnectionType: + list = append(list, validateBitbucketConnection(connection)...) + case provisioning.GitlabConnectionType: + list = append(list, validateGitlabConnection(connection)...) + default: + list = append( + list, field.NotSupported( + field.NewPath("spec", "type"), + connection.Spec.Type, + []provisioning.ConnectionType{ + provisioning.GithubConnectionType, + provisioning.BitbucketConnectionType, + provisioning.GitlabConnectionType, + }), + ) + } + + return toError(connection.GetName(), list) +} + +func validateGithubConnection(connection *provisioning.Connection) field.ErrorList { + list := field.ErrorList{} + + if connection.Spec.GitHub == nil { + list = append( + list, field.Required(field.NewPath("spec", "github"), "github info must be specified for GitHub connection"), + ) + } + + if connection.Secure.PrivateKey.IsZero() { + list = append(list, field.Required(field.NewPath("secure", "privateKey"), "privateKey must be specified for GitHub connection")) + } + if !connection.Secure.ClientSecret.IsZero() { + list = append(list, field.Forbidden(field.NewPath("secure", "clientSecret"), "clientSecret is forbidden in GitHub connection")) + } + + return list +} + +func validateBitbucketConnection(connection *provisioning.Connection) field.ErrorList { + list := field.ErrorList{} + + if connection.Spec.Bitbucket == nil { + list = append( + list, field.Required(field.NewPath("spec", "bitbucket"), "bitbucket info must be specified in Bitbucket connection"), + ) + } + if connection.Secure.ClientSecret.IsZero() { + list = append(list, field.Required(field.NewPath("secure", "clientSecret"), "clientSecret must be specified for Bitbucket connection")) + } + if !connection.Secure.PrivateKey.IsZero() { + list = append(list, field.Forbidden(field.NewPath("secure", "privateKey"), "privateKey is forbidden in Bitbucket connection")) + } + + return list +} + +func validateGitlabConnection(connection *provisioning.Connection) field.ErrorList { + list := field.ErrorList{} + + if connection.Spec.Gitlab == nil { + list = append( + list, field.Required(field.NewPath("spec", "gitlab"), "gitlab info must be specified in Gitlab connection"), + ) + } + if connection.Secure.ClientSecret.IsZero() { + list = append(list, field.Required(field.NewPath("secure", "clientSecret"), "clientSecret must be specified for Gitlab connection")) + } + if !connection.Secure.PrivateKey.IsZero() { + list = append(list, field.Forbidden(field.NewPath("secure", "privateKey"), "privateKey is forbidden in Gitlab connection")) + } + + return 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.ConnectionResourceInfo.GroupVersionKind().GroupKind(), + name, + list, + ) +} diff --git a/apps/provisioning/pkg/connection/validator_test.go b/apps/provisioning/pkg/connection/validator_test.go new file mode 100644 index 00000000000..23d4b01b800 --- /dev/null +++ b/apps/provisioning/pkg/connection/validator_test.go @@ -0,0 +1,253 @@ +package connection_test + +import ( + "testing" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/connection" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestValidateConnection(t *testing.T) { + tests := []struct { + name string + connection *provisioning.Connection + wantErr bool + errMsg string + }{ + { + name: "empty type returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{}, + }, + wantErr: true, + errMsg: "spec.type", + }, + { + name: "invalid type returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: "invalid", + }, + }, + wantErr: true, + errMsg: "spec.type", + }, + { + name: "github type without github config returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GithubConnectionType, + }, + }, + wantErr: true, + errMsg: "spec.github", + }, + { + name: "github type without private key returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GithubConnectionType, + GitHub: &provisioning.GitHubConnectionConfig{ + AppID: "123", + InstallationID: "456", + }, + }, + }, + wantErr: true, + errMsg: "secure.privateKey", + }, + { + name: "github type with client secret returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GithubConnectionType, + GitHub: &provisioning.GitHubConnectionConfig{ + AppID: "123", + InstallationID: "456", + }, + }, + Secure: provisioning.ConnectionSecure{ + PrivateKey: common.InlineSecureValue{ + Name: "test-private-key", + }, + ClientSecret: common.InlineSecureValue{ + Name: "test-client-secret", + }, + }, + }, + wantErr: true, + errMsg: "secure.clientSecret", + }, + { + name: "github type with github config is valid", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GithubConnectionType, + GitHub: &provisioning.GitHubConnectionConfig{ + AppID: "123", + InstallationID: "456", + }, + }, + Secure: provisioning.ConnectionSecure{ + PrivateKey: common.InlineSecureValue{ + Name: "test-private-key", + }, + }, + }, + wantErr: false, + }, + { + name: "bitbucket type without bitbucket config returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.BitbucketConnectionType, + }, + }, + wantErr: true, + errMsg: "spec.bitbucket", + }, + { + name: "bitbucket type without client secret returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.BitbucketConnectionType, + Bitbucket: &provisioning.BitbucketConnectionConfig{ + ClientID: "client-123", + }, + }, + }, + wantErr: true, + errMsg: "secure.clientSecret", + }, + { + name: "bitbucket type with private key returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.BitbucketConnectionType, + Bitbucket: &provisioning.BitbucketConnectionConfig{ + ClientID: "client-123", + }, + }, + Secure: provisioning.ConnectionSecure{ + PrivateKey: common.InlineSecureValue{ + Name: "test-private-key", + }, + ClientSecret: common.InlineSecureValue{ + Name: "test-client-secret", + }, + }, + }, + wantErr: true, + errMsg: "secure.privateKey", + }, + { + name: "bitbucket type with bitbucket config is valid", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.BitbucketConnectionType, + Bitbucket: &provisioning.BitbucketConnectionConfig{ + ClientID: "client-123", + }, + }, + Secure: provisioning.ConnectionSecure{ + ClientSecret: common.InlineSecureValue{ + Name: "test-client-secret", + }, + }, + }, + wantErr: false, + }, + { + name: "gitlab type without gitlab config returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GitlabConnectionType, + }, + }, + wantErr: true, + errMsg: "spec.gitlab", + }, + { + name: "gitlab type without client secret returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GitlabConnectionType, + Gitlab: &provisioning.GitlabConnectionConfig{ + ClientID: "client-456", + }, + }, + }, + wantErr: true, + errMsg: "secure.clientSecret", + }, + { + name: "gitlab type with private key returns error", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GitlabConnectionType, + Gitlab: &provisioning.GitlabConnectionConfig{ + ClientID: "client-456", + }, + }, + Secure: provisioning.ConnectionSecure{ + PrivateKey: common.InlineSecureValue{ + Name: "test-private-key", + }, + ClientSecret: common.InlineSecureValue{ + Name: "test-client-secret", + }, + }, + }, + wantErr: true, + errMsg: "secure.privateKey", + }, + { + name: "gitlab type with gitlab config is valid", + connection: &provisioning.Connection{ + ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, + Spec: provisioning.ConnectionSpec{ + Type: provisioning.GitlabConnectionType, + Gitlab: &provisioning.GitlabConnectionConfig{ + ClientID: "client-456", + }, + }, + Secure: provisioning.ConnectionSecure{ + ClientSecret: common.InlineSecureValue{ + Name: "test-client-secret", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := connection.ValidateConnection(tt.connection) + if tt.wantErr { + assert.Error(t, err) + if tt.errMsg != "" { + assert.Contains(t, err.Error(), tt.errMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketconnectionconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketconnectionconfig.go new file mode 100644 index 00000000000..9a3604afe71 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketconnectionconfig.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// BitbucketConnectionConfigApplyConfiguration represents a declarative configuration of the BitbucketConnectionConfig type for use +// with apply. +type BitbucketConnectionConfigApplyConfiguration struct { + ClientID *string `json:"clientID,omitempty"` +} + +// BitbucketConnectionConfigApplyConfiguration constructs a declarative configuration of the BitbucketConnectionConfig type for use with +// apply. +func BitbucketConnectionConfig() *BitbucketConnectionConfigApplyConfiguration { + return &BitbucketConnectionConfigApplyConfiguration{} +} + +// WithClientID sets the ClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientID field is set to the value of the last call. +func (b *BitbucketConnectionConfigApplyConfiguration) WithClientID(value string) *BitbucketConnectionConfigApplyConfiguration { + b.ClientID = &value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connection.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connection.go new file mode 100644 index 00000000000..a7a56c62a14 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connection.go @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ConnectionApplyConfiguration represents a declarative configuration of the Connection type for use +// with apply. +type ConnectionApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *ConnectionSpecApplyConfiguration `json:"spec,omitempty"` + Secure *ConnectionSecureApplyConfiguration `json:"secure,omitempty"` + Status *ConnectionStatusApplyConfiguration `json:"status,omitempty"` +} + +// Connection constructs a declarative configuration of the Connection type for use with +// apply. +func Connection(name, namespace string) *ConnectionApplyConfiguration { + b := &ConnectionApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Connection") + b.WithAPIVersion("provisioning.grafana.app/v0alpha1") + return b +} +func (b ConnectionApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithKind(value string) *ConnectionApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithAPIVersion(value string) *ConnectionApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithName(value string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithGenerateName(value string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithNamespace(value string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithUID(value types.UID) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithResourceVersion(value string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithGeneration(value int64) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithCreationTimestamp(value metav1.Time) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *ConnectionApplyConfiguration) WithLabels(entries map[string]string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *ConnectionApplyConfiguration) WithAnnotations(entries map[string]string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *ConnectionApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *ConnectionApplyConfiguration) WithFinalizers(values ...string) *ConnectionApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *ConnectionApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithSpec(value *ConnectionSpecApplyConfiguration) *ConnectionApplyConfiguration { + b.Spec = value + return b +} + +// WithSecure sets the Secure field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Secure field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithSecure(value *ConnectionSecureApplyConfiguration) *ConnectionApplyConfiguration { + b.Secure = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *ConnectionApplyConfiguration) WithStatus(value *ConnectionStatusApplyConfiguration) *ConnectionApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *ConnectionApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *ConnectionApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *ConnectionApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *ConnectionApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectioninfo.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectioninfo.go new file mode 100644 index 00000000000..87e4aa7e180 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectioninfo.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// ConnectionInfoApplyConfiguration represents a declarative configuration of the ConnectionInfo type for use +// with apply. +type ConnectionInfoApplyConfiguration struct { + Name *string `json:"name,omitempty"` +} + +// ConnectionInfoApplyConfiguration constructs a declarative configuration of the ConnectionInfo type for use with +// apply. +func ConnectionInfo() *ConnectionInfoApplyConfiguration { + return &ConnectionInfoApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ConnectionInfoApplyConfiguration) WithName(value string) *ConnectionInfoApplyConfiguration { + b.Name = &value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionsecure.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionsecure.go new file mode 100644 index 00000000000..8ac26b192c9 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionsecure.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// ConnectionSecureApplyConfiguration represents a declarative configuration of the ConnectionSecure type for use +// with apply. +type ConnectionSecureApplyConfiguration struct { + PrivateKey *commonv0alpha1.InlineSecureValue `json:"privateKey,omitempty"` + ClientSecret *commonv0alpha1.InlineSecureValue `json:"clientSecret,omitempty"` + Token *commonv0alpha1.InlineSecureValue `json:"webhook,omitempty"` +} + +// ConnectionSecureApplyConfiguration constructs a declarative configuration of the ConnectionSecure type for use with +// apply. +func ConnectionSecure() *ConnectionSecureApplyConfiguration { + return &ConnectionSecureApplyConfiguration{} +} + +// WithPrivateKey sets the PrivateKey field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PrivateKey field is set to the value of the last call. +func (b *ConnectionSecureApplyConfiguration) WithPrivateKey(value commonv0alpha1.InlineSecureValue) *ConnectionSecureApplyConfiguration { + b.PrivateKey = &value + return b +} + +// WithClientSecret sets the ClientSecret field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientSecret field is set to the value of the last call. +func (b *ConnectionSecureApplyConfiguration) WithClientSecret(value commonv0alpha1.InlineSecureValue) *ConnectionSecureApplyConfiguration { + b.ClientSecret = &value + return b +} + +// WithToken sets the Token field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Token field is set to the value of the last call. +func (b *ConnectionSecureApplyConfiguration) WithToken(value commonv0alpha1.InlineSecureValue) *ConnectionSecureApplyConfiguration { + b.Token = &value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionspec.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionspec.go new file mode 100644 index 00000000000..1b55b832ae1 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionspec.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +// ConnectionSpecApplyConfiguration represents a declarative configuration of the ConnectionSpec type for use +// with apply. +type ConnectionSpecApplyConfiguration struct { + Type *provisioningv0alpha1.ConnectionType `json:"type,omitempty"` + URL *string `json:"url,omitempty"` + GitHub *GitHubConnectionConfigApplyConfiguration `json:"github,omitempty"` + Bitbucket *BitbucketConnectionConfigApplyConfiguration `json:"bitbucket,omitempty"` + Gitlab *GitlabConnectionConfigApplyConfiguration `json:"gitlab,omitempty"` +} + +// ConnectionSpecApplyConfiguration constructs a declarative configuration of the ConnectionSpec type for use with +// apply. +func ConnectionSpec() *ConnectionSpecApplyConfiguration { + return &ConnectionSpecApplyConfiguration{} +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *ConnectionSpecApplyConfiguration) WithType(value provisioningv0alpha1.ConnectionType) *ConnectionSpecApplyConfiguration { + b.Type = &value + return b +} + +// WithURL sets the URL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the URL field is set to the value of the last call. +func (b *ConnectionSpecApplyConfiguration) WithURL(value string) *ConnectionSpecApplyConfiguration { + b.URL = &value + return b +} + +// WithGitHub sets the GitHub field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GitHub field is set to the value of the last call. +func (b *ConnectionSpecApplyConfiguration) WithGitHub(value *GitHubConnectionConfigApplyConfiguration) *ConnectionSpecApplyConfiguration { + b.GitHub = value + return b +} + +// WithBitbucket sets the Bitbucket field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Bitbucket field is set to the value of the last call. +func (b *ConnectionSpecApplyConfiguration) WithBitbucket(value *BitbucketConnectionConfigApplyConfiguration) *ConnectionSpecApplyConfiguration { + b.Bitbucket = value + return b +} + +// WithGitlab sets the Gitlab field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Gitlab field is set to the value of the last call. +func (b *ConnectionSpecApplyConfiguration) WithGitlab(value *GitlabConnectionConfigApplyConfiguration) *ConnectionSpecApplyConfiguration { + b.Gitlab = value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionstatus.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionstatus.go new file mode 100644 index 00000000000..b9d510aac91 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/connectionstatus.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +// ConnectionStatusApplyConfiguration represents a declarative configuration of the ConnectionStatus type for use +// with apply. +type ConnectionStatusApplyConfiguration struct { + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` + State *provisioningv0alpha1.ConnectionState `json:"state,omitempty"` + Health *HealthStatusApplyConfiguration `json:"health,omitempty"` +} + +// ConnectionStatusApplyConfiguration constructs a declarative configuration of the ConnectionStatus type for use with +// apply. +func ConnectionStatus() *ConnectionStatusApplyConfiguration { + return &ConnectionStatusApplyConfiguration{} +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *ConnectionStatusApplyConfiguration) WithObservedGeneration(value int64) *ConnectionStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} + +// WithState sets the State field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the State field is set to the value of the last call. +func (b *ConnectionStatusApplyConfiguration) WithState(value provisioningv0alpha1.ConnectionState) *ConnectionStatusApplyConfiguration { + b.State = &value + return b +} + +// WithHealth sets the Health field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Health field is set to the value of the last call. +func (b *ConnectionStatusApplyConfiguration) WithHealth(value *HealthStatusApplyConfiguration) *ConnectionStatusApplyConfiguration { + b.Health = value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubconnectionconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubconnectionconfig.go new file mode 100644 index 00000000000..c2f2a8b291b --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/githubconnectionconfig.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// GitHubConnectionConfigApplyConfiguration represents a declarative configuration of the GitHubConnectionConfig type for use +// with apply. +type GitHubConnectionConfigApplyConfiguration struct { + AppID *string `json:"appID,omitempty"` + InstallationID *string `json:"installationID,omitempty"` +} + +// GitHubConnectionConfigApplyConfiguration constructs a declarative configuration of the GitHubConnectionConfig type for use with +// apply. +func GitHubConnectionConfig() *GitHubConnectionConfigApplyConfiguration { + return &GitHubConnectionConfigApplyConfiguration{} +} + +// WithAppID sets the AppID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AppID field is set to the value of the last call. +func (b *GitHubConnectionConfigApplyConfiguration) WithAppID(value string) *GitHubConnectionConfigApplyConfiguration { + b.AppID = &value + return b +} + +// WithInstallationID sets the InstallationID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the InstallationID field is set to the value of the last call. +func (b *GitHubConnectionConfigApplyConfiguration) WithInstallationID(value string) *GitHubConnectionConfigApplyConfiguration { + b.InstallationID = &value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabconnectionconfig.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabconnectionconfig.go new file mode 100644 index 00000000000..0238ae3c226 --- /dev/null +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabconnectionconfig.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// GitlabConnectionConfigApplyConfiguration represents a declarative configuration of the GitlabConnectionConfig type for use +// with apply. +type GitlabConnectionConfigApplyConfiguration struct { + ClientID *string `json:"clientID,omitempty"` +} + +// GitlabConnectionConfigApplyConfiguration constructs a declarative configuration of the GitlabConnectionConfig type for use with +// apply. +func GitlabConnectionConfig() *GitlabConnectionConfigApplyConfiguration { + return &GitlabConnectionConfigApplyConfiguration{} +} + +// WithClientID sets the ClientID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientID field is set to the value of the last call. +func (b *GitlabConnectionConfigApplyConfiguration) WithClientID(value string) *GitlabConnectionConfigApplyConfiguration { + b.ClientID = &value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobresourcesummary.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobresourcesummary.go index ed6a62f651a..8986d2f85a6 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobresourcesummary.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobresourcesummary.go @@ -7,16 +7,18 @@ package v0alpha1 // JobResourceSummaryApplyConfiguration represents a declarative configuration of the JobResourceSummary type for use // with apply. type JobResourceSummaryApplyConfiguration struct { - Group *string `json:"group,omitempty"` - Kind *string `json:"kind,omitempty"` - Total *int64 `json:"total,omitempty"` - Create *int64 `json:"create,omitempty"` - Update *int64 `json:"update,omitempty"` - Delete *int64 `json:"delete,omitempty"` - Write *int64 `json:"write,omitempty"` - Error *int64 `json:"error,omitempty"` - Noop *int64 `json:"noop,omitempty"` - Errors []string `json:"errors,omitempty"` + Group *string `json:"group,omitempty"` + Kind *string `json:"kind,omitempty"` + Total *int64 `json:"total,omitempty"` + Create *int64 `json:"create,omitempty"` + Update *int64 `json:"update,omitempty"` + Delete *int64 `json:"delete,omitempty"` + Write *int64 `json:"write,omitempty"` + Error *int64 `json:"error,omitempty"` + Warning *int64 `json:"warning,omitempty"` + Noop *int64 `json:"noop,omitempty"` + Errors []string `json:"errors,omitempty"` + Warnings []string `json:"warnings,omitempty"` } // JobResourceSummaryApplyConfiguration constructs a declarative configuration of the JobResourceSummary type for use with @@ -89,6 +91,14 @@ func (b *JobResourceSummaryApplyConfiguration) WithError(value int64) *JobResour return b } +// WithWarning sets the Warning field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Warning field is set to the value of the last call. +func (b *JobResourceSummaryApplyConfiguration) WithWarning(value int64) *JobResourceSummaryApplyConfiguration { + b.Warning = &value + return b +} + // WithNoop sets the Noop field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Noop field is set to the value of the last call. @@ -106,3 +116,13 @@ func (b *JobResourceSummaryApplyConfiguration) WithErrors(values ...string) *Job } return b } + +// WithWarnings adds the given value to the Warnings field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Warnings field. +func (b *JobResourceSummaryApplyConfiguration) WithWarnings(values ...string) *JobResourceSummaryApplyConfiguration { + for i := range values { + b.Warnings = append(b.Warnings, values[i]) + } + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobstatus.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobstatus.go index ea9228473a5..0ad62090c62 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobstatus.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/jobstatus.go @@ -16,6 +16,7 @@ type JobStatusApplyConfiguration struct { Finished *int64 `json:"finished,omitempty"` Message *string `json:"message,omitempty"` Errors []string `json:"errors,omitempty"` + Warnings []string `json:"warnings,omitempty"` Progress *float64 `json:"progress,omitempty"` Summary []*provisioningv0alpha1.JobResourceSummary `json:"summary,omitempty"` URLs *RepositoryURLsApplyConfiguration `json:"url,omitempty"` @@ -69,6 +70,16 @@ func (b *JobStatusApplyConfiguration) WithErrors(values ...string) *JobStatusApp return b } +// WithWarnings adds the given value to the Warnings field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Warnings field. +func (b *JobStatusApplyConfiguration) WithWarnings(values ...string) *JobStatusApplyConfiguration { + for i := range values { + b.Warnings = append(b.Warnings, values[i]) + } + return b +} + // WithProgress sets the Progress field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Progress field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/migratejoboptions.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/migratejoboptions.go index aee0b05fa3a..7f689c19585 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/migratejoboptions.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/migratejoboptions.go @@ -7,7 +7,6 @@ package v0alpha1 // MigrateJobOptionsApplyConfiguration represents a declarative configuration of the MigrateJobOptions type for use // with apply. type MigrateJobOptionsApplyConfiguration struct { - History *bool `json:"history,omitempty"` Message *string `json:"message,omitempty"` } @@ -17,14 +16,6 @@ func MigrateJobOptions() *MigrateJobOptionsApplyConfiguration { return &MigrateJobOptionsApplyConfiguration{} } -// WithHistory sets the History field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the History field is set to the value of the last call. -func (b *MigrateJobOptionsApplyConfiguration) WithHistory(value bool) *MigrateJobOptionsApplyConfiguration { - b.History = &value - return b -} - // WithMessage sets the Message field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Message field is set to the value of the last call. diff --git a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go index 6fff6f2de42..5caf805702b 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go @@ -21,6 +21,7 @@ type RepositorySpecApplyConfiguration struct { Git *GitRepositoryConfigApplyConfiguration `json:"git,omitempty"` Bitbucket *BitbucketRepositoryConfigApplyConfiguration `json:"bitbucket,omitempty"` GitLab *GitLabRepositoryConfigApplyConfiguration `json:"gitlab,omitempty"` + Connection *ConnectionInfoApplyConfiguration `json:"connection,omitempty"` } // RepositorySpecApplyConfiguration constructs a declarative configuration of the RepositorySpec type for use with @@ -110,3 +111,11 @@ func (b *RepositorySpecApplyConfiguration) WithGitLab(value *GitLabRepositoryCon b.GitLab = value return b } + +// WithConnection sets the Connection field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Connection field is set to the value of the last call. +func (b *RepositorySpecApplyConfiguration) WithConnection(value *ConnectionInfoApplyConfiguration) *RepositorySpecApplyConfiguration { + b.Connection = value + return b +} diff --git a/apps/provisioning/pkg/generated/applyconfiguration/utils.go b/apps/provisioning/pkg/generated/applyconfiguration/utils.go index 400f2b7c083..7168418b82d 100644 --- a/apps/provisioning/pkg/generated/applyconfiguration/utils.go +++ b/apps/provisioning/pkg/generated/applyconfiguration/utils.go @@ -18,14 +18,30 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=provisioning.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithKind("BitbucketConnectionConfig"): + return &provisioningv0alpha1.BitbucketConnectionConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("BitbucketRepositoryConfig"): return &provisioningv0alpha1.BitbucketRepositoryConfigApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("Connection"): + return &provisioningv0alpha1.ConnectionApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("ConnectionInfo"): + return &provisioningv0alpha1.ConnectionInfoApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("ConnectionSecure"): + return &provisioningv0alpha1.ConnectionSecureApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("ConnectionSpec"): + return &provisioningv0alpha1.ConnectionSpecApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("ConnectionStatus"): + return &provisioningv0alpha1.ConnectionStatusApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("DeleteJobOptions"): return &provisioningv0alpha1.DeleteJobOptionsApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("ExportJobOptions"): return &provisioningv0alpha1.ExportJobOptionsApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("GitHubConnectionConfig"): + return &provisioningv0alpha1.GitHubConnectionConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("GitHubRepositoryConfig"): return &provisioningv0alpha1.GitHubRepositoryConfigApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("GitlabConnectionConfig"): + return &provisioningv0alpha1.GitlabConnectionConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("GitLabRepositoryConfig"): return &provisioningv0alpha1.GitLabRepositoryConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("GitRepositoryConfig"): diff --git a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/connection.go b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/connection.go new file mode 100644 index 00000000000..fe585e5c0bb --- /dev/null +++ b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/connection.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + context "context" + + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + applyconfigurationprovisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1" + scheme "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// ConnectionsGetter has a method to return a ConnectionInterface. +// A group's client should implement this interface. +type ConnectionsGetter interface { + Connections(namespace string) ConnectionInterface +} + +// ConnectionInterface has methods to work with Connection resources. +type ConnectionInterface interface { + Create(ctx context.Context, connection *provisioningv0alpha1.Connection, opts v1.CreateOptions) (*provisioningv0alpha1.Connection, error) + Update(ctx context.Context, connection *provisioningv0alpha1.Connection, opts v1.UpdateOptions) (*provisioningv0alpha1.Connection, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, connection *provisioningv0alpha1.Connection, opts v1.UpdateOptions) (*provisioningv0alpha1.Connection, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*provisioningv0alpha1.Connection, error) + List(ctx context.Context, opts v1.ListOptions) (*provisioningv0alpha1.ConnectionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *provisioningv0alpha1.Connection, err error) + Apply(ctx context.Context, connection *applyconfigurationprovisioningv0alpha1.ConnectionApplyConfiguration, opts v1.ApplyOptions) (result *provisioningv0alpha1.Connection, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, connection *applyconfigurationprovisioningv0alpha1.ConnectionApplyConfiguration, opts v1.ApplyOptions) (result *provisioningv0alpha1.Connection, err error) + ConnectionExpansion +} + +// connections implements ConnectionInterface +type connections struct { + *gentype.ClientWithListAndApply[*provisioningv0alpha1.Connection, *provisioningv0alpha1.ConnectionList, *applyconfigurationprovisioningv0alpha1.ConnectionApplyConfiguration] +} + +// newConnections returns a Connections +func newConnections(c *ProvisioningV0alpha1Client, namespace string) *connections { + return &connections{ + gentype.NewClientWithListAndApply[*provisioningv0alpha1.Connection, *provisioningv0alpha1.ConnectionList, *applyconfigurationprovisioningv0alpha1.ConnectionApplyConfiguration]( + "connections", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *provisioningv0alpha1.Connection { return &provisioningv0alpha1.Connection{} }, + func() *provisioningv0alpha1.ConnectionList { return &provisioningv0alpha1.ConnectionList{} }, + ), + } +} diff --git a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_connection.go b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_connection.go new file mode 100644 index 00000000000..2059d044172 --- /dev/null +++ b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_connection.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1" + typedprovisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeConnections implements ConnectionInterface +type fakeConnections struct { + *gentype.FakeClientWithListAndApply[*v0alpha1.Connection, *v0alpha1.ConnectionList, *provisioningv0alpha1.ConnectionApplyConfiguration] + Fake *FakeProvisioningV0alpha1 +} + +func newFakeConnections(fake *FakeProvisioningV0alpha1, namespace string) typedprovisioningv0alpha1.ConnectionInterface { + return &fakeConnections{ + gentype.NewFakeClientWithListAndApply[*v0alpha1.Connection, *v0alpha1.ConnectionList, *provisioningv0alpha1.ConnectionApplyConfiguration]( + fake.Fake, + namespace, + v0alpha1.SchemeGroupVersion.WithResource("connections"), + v0alpha1.SchemeGroupVersion.WithKind("Connection"), + func() *v0alpha1.Connection { return &v0alpha1.Connection{} }, + func() *v0alpha1.ConnectionList { return &v0alpha1.ConnectionList{} }, + func(dst, src *v0alpha1.ConnectionList) { dst.ListMeta = src.ListMeta }, + func(list *v0alpha1.ConnectionList) []*v0alpha1.Connection { return gentype.ToPointerSlice(list.Items) }, + func(list *v0alpha1.ConnectionList, items []*v0alpha1.Connection) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_provisioning_client.go b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_provisioning_client.go index d6fe94156be..2f1422cadc1 100644 --- a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_provisioning_client.go +++ b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/fake/fake_provisioning_client.go @@ -14,6 +14,10 @@ type FakeProvisioningV0alpha1 struct { *testing.Fake } +func (c *FakeProvisioningV0alpha1) Connections(namespace string) v0alpha1.ConnectionInterface { + return newFakeConnections(c, namespace) +} + func (c *FakeProvisioningV0alpha1) HistoricJobs(namespace string) v0alpha1.HistoricJobInterface { return newFakeHistoricJobs(c, namespace) } diff --git a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/generated_expansion.go b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/generated_expansion.go index 21b7a18b414..5220accc813 100644 --- a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/generated_expansion.go +++ b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/generated_expansion.go @@ -4,6 +4,8 @@ package v0alpha1 +type ConnectionExpansion interface{} + type HistoricJobExpansion interface{} type JobExpansion interface{} diff --git a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/provisioning_client.go b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/provisioning_client.go index 401bc533cfe..03a1e2e0ceb 100644 --- a/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/provisioning_client.go +++ b/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1/provisioning_client.go @@ -14,6 +14,7 @@ import ( type ProvisioningV0alpha1Interface interface { RESTClient() rest.Interface + ConnectionsGetter HistoricJobsGetter JobsGetter RepositoriesGetter @@ -24,6 +25,10 @@ type ProvisioningV0alpha1Client struct { restClient rest.Interface } +func (c *ProvisioningV0alpha1Client) Connections(namespace string) ConnectionInterface { + return newConnections(c, namespace) +} + func (c *ProvisioningV0alpha1Client) HistoricJobs(namespace string) HistoricJobInterface { return newHistoricJobs(c, namespace) } diff --git a/apps/provisioning/pkg/generated/informers/externalversions/generic.go b/apps/provisioning/pkg/generated/informers/externalversions/generic.go index 4a62aab044a..8429b75d959 100644 --- a/apps/provisioning/pkg/generated/informers/externalversions/generic.go +++ b/apps/provisioning/pkg/generated/informers/externalversions/generic.go @@ -39,6 +39,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=provisioning.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithResource("connections"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Provisioning().V0alpha1().Connections().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("historicjobs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Provisioning().V0alpha1().HistoricJobs().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("jobs"): diff --git a/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/connection.go b/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/connection.go new file mode 100644 index 00000000000..d8fa071219f --- /dev/null +++ b/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/connection.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + context "context" + time "time" + + apisprovisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + versioned "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned" + internalinterfaces "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions/internalinterfaces" + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ConnectionInformer provides access to a shared informer and lister for +// Connections. +type ConnectionInformer interface { + Informer() cache.SharedIndexInformer + Lister() provisioningv0alpha1.ConnectionLister +} + +type connectionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewConnectionInformer constructs a new informer for Connection type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewConnectionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredConnectionInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredConnectionInformer constructs a new informer for Connection type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredConnectionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ProvisioningV0alpha1().Connections(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ProvisioningV0alpha1().Connections(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ProvisioningV0alpha1().Connections(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ProvisioningV0alpha1().Connections(namespace).Watch(ctx, options) + }, + }, + &apisprovisioningv0alpha1.Connection{}, + resyncPeriod, + indexers, + ) +} + +func (f *connectionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredConnectionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *connectionInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisprovisioningv0alpha1.Connection{}, f.defaultInformer) +} + +func (f *connectionInformer) Lister() provisioningv0alpha1.ConnectionLister { + return provisioningv0alpha1.NewConnectionLister(f.Informer().GetIndexer()) +} diff --git a/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/interface.go b/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/interface.go index cbe2fcefaf3..1a1908ab5f3 100644 --- a/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/interface.go +++ b/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1/interface.go @@ -10,6 +10,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Connections returns a ConnectionInformer. + Connections() ConnectionInformer // HistoricJobs returns a HistoricJobInformer. HistoricJobs() HistoricJobInformer // Jobs returns a JobInformer. @@ -29,6 +31,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Connections returns a ConnectionInformer. +func (v *version) Connections() ConnectionInformer { + return &connectionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // HistoricJobs returns a HistoricJobInformer. func (v *version) HistoricJobs() HistoricJobInformer { return &historicJobInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/connection.go b/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/connection.go new file mode 100644 index 00000000000..a12902c2bea --- /dev/null +++ b/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/connection.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// ConnectionLister helps list Connections. +// All objects returned here must be treated as read-only. +type ConnectionLister interface { + // List lists all Connections in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*provisioningv0alpha1.Connection, err error) + // Connections returns an object that can list and get Connections. + Connections(namespace string) ConnectionNamespaceLister + ConnectionListerExpansion +} + +// connectionLister implements the ConnectionLister interface. +type connectionLister struct { + listers.ResourceIndexer[*provisioningv0alpha1.Connection] +} + +// NewConnectionLister returns a new ConnectionLister. +func NewConnectionLister(indexer cache.Indexer) ConnectionLister { + return &connectionLister{listers.New[*provisioningv0alpha1.Connection](indexer, provisioningv0alpha1.Resource("connection"))} +} + +// Connections returns an object that can list and get Connections. +func (s *connectionLister) Connections(namespace string) ConnectionNamespaceLister { + return connectionNamespaceLister{listers.NewNamespaced[*provisioningv0alpha1.Connection](s.ResourceIndexer, namespace)} +} + +// ConnectionNamespaceLister helps list and get Connections. +// All objects returned here must be treated as read-only. +type ConnectionNamespaceLister interface { + // List lists all Connections in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*provisioningv0alpha1.Connection, err error) + // Get retrieves the Connection from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*provisioningv0alpha1.Connection, error) + ConnectionNamespaceListerExpansion +} + +// connectionNamespaceLister implements the ConnectionNamespaceLister +// interface. +type connectionNamespaceLister struct { + listers.ResourceIndexer[*provisioningv0alpha1.Connection] +} diff --git a/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/expansion_generated.go b/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/expansion_generated.go index 7f0649542b3..4d840b6e2db 100644 --- a/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/expansion_generated.go +++ b/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1/expansion_generated.go @@ -4,6 +4,14 @@ package v0alpha1 +// ConnectionListerExpansion allows custom methods to be added to +// ConnectionLister. +type ConnectionListerExpansion interface{} + +// ConnectionNamespaceListerExpansion allows custom methods to be added to +// ConnectionNamespaceLister. +type ConnectionNamespaceListerExpansion interface{} + // HistoricJobListerExpansion allows custom methods to be added to // HistoricJobLister. type HistoricJobListerExpansion interface{} diff --git a/apps/provisioning/pkg/jobs/validator_test.go b/apps/provisioning/pkg/jobs/validator_test.go index fdd29598ebc..172e1929e1e 100644 --- a/apps/provisioning/pkg/jobs/validator_test.go +++ b/apps/provisioning/pkg/jobs/validator_test.go @@ -384,8 +384,7 @@ func TestValidateJob(t *testing.T) { Action: provisioning.JobActionMigrate, Repository: "test-repo", Migrate: &provisioning.MigrateJobOptions{ - History: true, - Message: "Migrate from legacy", + Message: "Migrate from unified", }, }, }, diff --git a/apps/provisioning/pkg/repository/git/repository.go b/apps/provisioning/pkg/repository/git/repository.go index 82d5ca6bd1d..bac3bd6d0a6 100644 --- a/apps/provisioning/pkg/repository/git/repository.go +++ b/apps/provisioning/pkg/repository/git/repository.go @@ -238,6 +238,8 @@ func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*reposi // Check if the path represents a directory if safepath.IsDir(filePath) { + // Strip trailing slash for git tree lookup to avoid empty path components + finalPath = strings.TrimSuffix(finalPath, "/") tree, err := r.client.GetTreeByPath(ctx, commit.Tree, finalPath) if err != nil { if errors.Is(err, nanogit.ErrObjectNotFound) { diff --git a/apps/provisioning/pkg/repository/tester.go b/apps/provisioning/pkg/repository/tester.go index 6bd65443d92..14290d0514b 100644 --- a/apps/provisioning/pkg/repository/tester.go +++ b/apps/provisioning/pkg/repository/tester.go @@ -23,7 +23,13 @@ func NewSimpleRepositoryTester(validator RepositoryValidator) SimpleRepositoryTe // TestRepository validates the repository and then runs a health check func (t *SimpleRepositoryTester) TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { - errors := t.validator.ValidateRepository(repo) + // Determine if this is a CREATE or UPDATE operation + // If the repository has been observed by the controller (ObservedGeneration > 0), + // it's an existing repository and we should treat it as UPDATE + cfg := repo.Config() + isCreate := cfg.Status.ObservedGeneration == 0 + + errors := t.validator.ValidateRepository(repo, isCreate) if len(errors) > 0 { rsp := &provisioning.TestResults{ Code: http.StatusUnprocessableEntity, // Invalid diff --git a/apps/provisioning/pkg/repository/validator.go b/apps/provisioning/pkg/repository/validator.go index 73198f0d7ee..46ba81f8cbd 100644 --- a/apps/provisioning/pkg/repository/validator.go +++ b/apps/provisioning/pkg/repository/validator.go @@ -32,7 +32,9 @@ func NewValidator(minSyncInterval time.Duration, allowedTargets []provisioning.S } // ValidateRepository solely does configuration checks on the repository object. It does not run a health check or compare against existing repositories. -func (v *RepositoryValidator) ValidateRepository(repo Repository) field.ErrorList { +// isCreate indicates whether this is a CREATE operation (true) or UPDATE operation (false). +// When isCreate is false, allowedTargets validation is skipped to allow existing repositories to continue working. +func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool) field.ErrorList { list := repo.Validate() cfg := repo.Config() @@ -44,7 +46,7 @@ func (v *RepositoryValidator) ValidateRepository(repo Repository) field.ErrorLis if cfg.Spec.Sync.Target == "" { list = append(list, field.Required(field.NewPath("spec", "sync", "target"), "The target type is required when sync is enabled")) - } else if !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) { + } else if isCreate && !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) { list = append(list, field.Invalid( field.NewPath("spec", "target"), diff --git a/apps/provisioning/pkg/repository/validator_test.go b/apps/provisioning/pkg/repository/validator_test.go index cb8b726a9bf..75980733e17 100644 --- a/apps/provisioning/pkg/repository/validator_test.go +++ b/apps/provisioning/pkg/repository/validator_test.go @@ -303,7 +303,8 @@ func TestValidateRepository(t *testing.T) { validator := NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder}, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - errors := validator.ValidateRepository(tt.repository) + // Tests validate new configurations, so always pass isCreate=true + errors := validator.ValidateRepository(tt.repository, true) require.Len(t, errors, tt.expectedErrs) if tt.validateError != nil { tt.validateError(t, errors) diff --git a/apps/quotas/Makefile b/apps/quotas/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/quotas/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/quotas/go.mod b/apps/quotas/go.mod new file mode 100644 index 00000000000..857521f2c57 --- /dev/null +++ b/apps/quotas/go.mod @@ -0,0 +1,92 @@ +module github.com/grafana/grafana/apps/quotas + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // 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.1 // 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.3 // 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.1 // 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.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.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.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/quotas/go.sum b/apps/quotas/go.sum new file mode 100644 index 00000000000..6bbf64cb454 --- /dev/null +++ b/apps/quotas/go.sum @@ -0,0 +1,282 @@ +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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +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/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +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 v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +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/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +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/gnostic-models v0.7.1/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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= +github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +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-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +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/mailru/easyjson v0.9.1/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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +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/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +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 v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +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/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +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/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +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/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +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 v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +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.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +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/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +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/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +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 v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11/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.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +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/kube-openapi v0.0.0-20251125145642-4e65d59e963e/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= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/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/json v0.0.0-20250730193827-2d320260d730/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/structured-merge-diff/v6 v6.3.1/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/quotas/kinds/cue.mod/module.cue b/apps/quotas/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..6e70f424d2e --- /dev/null +++ b/apps/quotas/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/quotas/kinds" +language: version: "v0.8.2" diff --git a/apps/quotas/kinds/manifest.cue b/apps/quotas/kinds/manifest.cue new file mode 100644 index 00000000000..8f799ff070c --- /dev/null +++ b/apps/quotas/kinds/manifest.cue @@ -0,0 +1,92 @@ +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: "quotas" + // 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: "quotas.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 + } + // 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"] + // } + ] + } +} + +// 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. +v0alpha1: { + // kinds is the list of kinds served by this version + kinds: [] + // [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 + + routes: { + // namespaced contains namespace-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/namespaces//'. + namespaced: { + "/usage": { + "GET": { + response: { + namespace: string + resource: string + group: string + usage: int64 + limit: int64 + } + request: { + query: { + group: string + resource: string + } + } + } + } + } + } + + // [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/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go new file mode 100644 index 00000000000..b19b40d5e02 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetUsageRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetUsageRequestParams `json:",inline"` +} + +func NewGetUsageRequestParamsObject() *GetUsageRequestParamsObject { + return &GetUsageRequestParamsObject{} +} + +func (o *GetUsageRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetUsageRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetUsageRequestParamsObject) DeepCopyInto(dst *GetUsageRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetUsageRequestParams := GetUsageRequestParams{} + _ = resource.CopyObjectInto(&dstGetUsageRequestParams, &o.GetUsageRequestParams) +} + +var _ runtime.Object = NewGetUsageRequestParamsObject() diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go new file mode 100644 index 00000000000..45394a7f20f --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +type GetUsageRequestParams struct { + Group string `json:"group"` + Resource string `json:"resource"` +} + +// NewGetUsageRequestParams creates a new GetUsageRequestParams object. +func NewGetUsageRequestParams() *GetUsageRequestParams { + return &GetUsageRequestParams{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go new file mode 100644 index 00000000000..eb87d022edd --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go @@ -0,0 +1,17 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type GetUsageBody struct { + Namespace string `json:"namespace"` + Resource string `json:"resource"` + Group string `json:"group"` + Usage int64 `json:"usage"` + Limit int64 `json:"limit"` +} + +// NewGetUsageBody creates a new GetUsageBody object. +func NewGetUsageBody() *GetUsageBody { + return &GetUsageBody{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go new file mode 100644 index 00000000000..87d6be2e587 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +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 GetUsage struct { + metav1.TypeMeta `json:",inline"` + GetUsageBody `json:",inline"` +} + +func NewGetUsage() *GetUsage { + return &GetUsage{} +} + +func (t *GetUsageBody) DeepCopyInto(dst *GetUsageBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetUsage) DeepCopyObject() runtime.Object { + dst := NewGetUsage() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetUsage) DeepCopyInto(dst *GetUsage) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetUsageBody.DeepCopyInto(&dst.GetUsageBody) +} + +var _ runtime.Object = NewGetUsage() diff --git a/apps/quotas/pkg/apis/quotas_manifest.go b/apps/quotas/pkg/apis/quotas_manifest.go new file mode 100644 index 00000000000..e72524d59f3 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas_manifest.go @@ -0,0 +1,213 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "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/quotas/pkg/apis/quotas/v0alpha1" +) + +var appManifestData = app.ManifestData{ + AppName: "quotas", + Group: "quotas.grafana.app", + PreferredVersion: "v0alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{}, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{ + "/usage": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getUsage", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "group", + In: "query", + Required: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "resource", + In: "query", + Required: true, + 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", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "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", + }, + }, + "limit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "usage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + }, + Required: []string{ + "namespace", + "resource", + "group", + "usage", + "limit", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + 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("quotas") +} + +var kindVersionToGoType = map[string]resource.Kind{} + +// 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{ + "v0alpha1||/usage|GET": v0alpha1.GetUsage{}, +} + +// 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/quotas/pkg/app/app.go b/apps/quotas/pkg/app/app.go new file mode 100644 index 00000000000..d4862661b60 --- /dev/null +++ b/apps/quotas/pkg/app/app.go @@ -0,0 +1,123 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "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/pkg/storage/unified/resourcepb" + + unifiedStorage "github.com/grafana/grafana/pkg/storage/unified/resource" + + "github.com/grafana/grafana-app-sdk/simple" + quotasv0alpha1 "github.com/grafana/grafana/apps/quotas/pkg/apis/quotas/v0alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type QuotasAppConfig struct { + ResourceClient unifiedStorage.ResourceClient +} + +type QuotasHandler struct { + ResourceClient unifiedStorage.ResourceClient +} + +func NewQuotasHandler(cfg *QuotasAppConfig) *QuotasHandler { + return &QuotasHandler{ + ResourceClient: cfg.ResourceClient, + } +} + +// GetQuota handles requests for the GET /usage resource route +func (h *QuotasHandler) GetQuota(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + if !request.URL.Query().Has("group") { + // TODO its returning a 500 instead of 400 bad request + writer.WriteHeader(http.StatusBadRequest) + return fmt.Errorf("missing required query parameters: group") + } + if !request.URL.Query().Has("resource") { + writer.WriteHeader(http.StatusBadRequest) + return fmt.Errorf("missing required query parameters: resource") + } + group := request.URL.Query().Get("group") + res := request.URL.Query().Get("resource") + + quotaReq := &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: request.ResourceIdentifier.Namespace, + Group: group, + Resource: res, + }, + } + quota, err := h.ResourceClient.GetQuotaUsage(ctx, quotaReq) + if err != nil { + return err + } + + writer.Header().Set("Content-Type", "application/json") + return json.NewEncoder(writer).Encode(quotasv0alpha1.GetUsage{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "quotas.grafana.com/v0alpha1", + Kind: "Quotas", + }, + GetUsageBody: quotasv0alpha1.GetUsageBody{ + Namespace: request.ResourceIdentifier.Namespace, + Group: group, + Resource: res, + Usage: quota.Usage, + Limit: quota.Limit, + }, + }) +} + +func New(cfg app.Config) (app.App, error) { + appConfig, ok := cfg.SpecificConfig.(*QuotasAppConfig) + if !ok { + return nil, fmt.Errorf("expected QuotasAppConfig but got %T", cfg.SpecificConfig) + } + handler := NewQuotasHandler(appConfig) + + simpleConfig := simple.AppConfig{ + Name: "quotas", + 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{}, + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v0alpha1": { + { + Namespaced: true, + Path: "usage", + Method: "GET", + }: handler.GetQuota, + }, + }, + } + + 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 { + return map[schema.GroupVersion][]resource.Kind{} +} diff --git a/apps/quotas/pkg/app/app_test.go b/apps/quotas/pkg/app/app_test.go new file mode 100644 index 00000000000..252c2c4e240 --- /dev/null +++ b/apps/quotas/pkg/app/app_test.go @@ -0,0 +1,71 @@ +package app + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetQuota(t *testing.T) { + t.Run("will return error when resource param is missing", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.Error(t, err) + }) + + t.Run("will return error when group param is missing", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?resource=dashboards") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.Error(t, err) + }) + + t.Run("will return quotas response when params are valid", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + clientMock.On("GetQuotaUsage", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.QuotaUsageResponse{ + Error: nil, + Usage: 1, + Limit: 2, + }, nil) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app&resource=dashboards") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.NoError(t, err) + + require.Equal(t, 200, recorder.Code) + }) +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts new file mode 100644 index 00000000000..70f306a1b08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_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 Quota { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/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/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..9209753fa64 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + count: string; + limit: string; + kind: string; +} + +export const defaultSpec = (): Spec => ({ + count: "", + limit: "", + kind: "", +}); + diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/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/scope/go.mod b/apps/scope/go.mod index c54a582379f..ed380519e75 100644 --- a/apps/scope/go.mod +++ b/apps/scope/go.mod @@ -4,8 +4,8 @@ go 1.25.5 require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( @@ -13,15 +13,24 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.7.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/kr/pretty v0.3.1 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -30,14 +39,15 @@ 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.47.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.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 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/scope/go.sum b/apps/scope/go.sum index 5df9519a7e2..19f7f445bc6 100644 --- a/apps/scope/go.sum +++ b/apps/scope/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -8,44 +9,71 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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-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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a h1:L7xgV9mP6MRF3L2/vDOjNR7heaBPbXPMGTDN9/jXSFQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a/go.mod h1:OK8NwS87D5YphchOcAsiIWk/feMZ0EzfAGME1Kff860= -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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= @@ -73,8 +101,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -83,8 +111,8 @@ 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/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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -93,8 +121,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -102,16 +130,16 @@ 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/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/scope/pkg/apis/scope/v0alpha1/types.go b/apps/scope/pkg/apis/scope/v0alpha1/types.go index 0c323d8d14b..20414f3caf6 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/types.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/types.go @@ -211,6 +211,12 @@ type ScopeNavigationSpec struct { Scope string `json:"scope"` // Used to navigate to a sub-scope of the main scope. URL will not be used if this is set. SubScope string `json:"subScope,omitempty"` + // Preload the subscope children, as soon as the ScopeNavigation is loaded. + PreLoadSubScopeChildren bool `json:"preLoadSubScopeChildren,omitempty"` + // Expands to display the subscope children when the ScopeNavigation is loaded. + ExpandOnLoad bool `json:"expandOnLoad,omitempty"` + // Makes the subscope not selectable, only serving as a way to build the tree. + DisableSubScopeSelection bool `json:"disableSubScopeSelection,omitempty"` } // Type of the item. 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 50015307139..1cb72adf4b1 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -642,6 +642,27 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNavigationSpec(ref common.ReferenceCall Format: "", }, }, + "preLoadSubScopeChildren": { + SchemaProps: spec.SchemaProps{ + Description: "Preload the subscope children, as soon as the ScopeNavigation is loaded.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "expandOnLoad": { + SchemaProps: spec.SchemaProps{ + Description: "Expands to display the subscope children when the ScopeNavigation is loaded.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "disableSubScopeSelection": { + SchemaProps: spec.SchemaProps{ + Description: "Makes the subscope not selectable, only serving as a way to build the tree.", + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"url", "scope"}, }, diff --git a/apps/sdk.mk b/apps/sdk.mk index 68cf4c1c9c9..45580f59037 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.5 +APP_SDK_VERSION = v0.48.7 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 a1ed48be6e7..d0da36e2e83 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,15 +3,15 @@ module github.com/grafana/grafana/apps/secret go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 google.golang.org/grpc v1.77.0 - google.golang.org/protobuf v1.36.10 - k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + google.golang.org/protobuf v1.36.11 + k8s.io/apimachinery v0.34.3 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ) require ( @@ -22,19 +22,29 @@ require ( 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-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/grafana/grafana-app-sdk/logging v0.48.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.7 // 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/mailru/easyjson v0.9.1 // 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 @@ -45,26 +55,27 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/client-go v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 166e281f5af..6ea5dd8dfd3 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -16,31 +16,57 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -62,8 +88,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -80,8 +106,6 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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= @@ -89,10 +113,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -105,24 +129,24 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -138,24 +162,24 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -168,35 +192,35 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 4a414de5b4e..c75106d2822 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,18 +3,18 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.5 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk v0.48.7 + github.com/grafana/grafana-app-sdk/logging v0.48.7 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 k8s.io/klog/v2 v2.130.1 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // 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 @@ -25,13 +25,23 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gnostic-models v0.7.1 // 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 @@ -43,7 +53,7 @@ require ( 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/mailru/easyjson v0.9.1 // 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 @@ -57,42 +67,42 @@ 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.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // 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/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // 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/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.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.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.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-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index 58ecc31fd82..36b5842b179 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -1,7 +1,7 @@ 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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/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= @@ -25,14 +25,40 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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= @@ -41,8 +67,8 @@ 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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -56,10 +82,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= @@ -83,8 +109,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/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= @@ -116,10 +142,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -134,30 +160,30 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -175,34 +201,34 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -211,41 +237,41 @@ 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/conf/defaults.ini b/conf/defaults.ini index a72b0183290..363ca39d0c4 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -335,6 +335,9 @@ rudderstack_data_plane_url = # Rudderstack SDK url, optional, only valid if rudderstack_write_key and rudderstack_data_plane_url is also set rudderstack_sdk_url = +# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 +rudderstack_v3_sdk_url = + # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config rudderstack_config_url = @@ -1327,6 +1330,10 @@ alertmanager_max_silences_count = # Maximum silence size in bytes. Default: 0 (no limit). alertmanager_max_silence_size_bytes = +# Maximum size of the expanded template output in bytes. Default: 10485760 (0 - no limit). +# The result of template expansion will be truncated to the limit. +alertmanager_max_template_output_bytes = + # Redis server address or addresses. It can be a single Redis address if using Redis standalone, # or a list of comma-separated addresses if using Redis Cluster/Sentinel. ha_redis_address = @@ -1646,6 +1653,11 @@ loki_basic_auth_password = # Accepts duration formats like: 30s, 1m, 1h. rule_query_offset = 1m +# Default data source UID to use for query execution when importing Prometheus rules. +# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided. +# If not set, the header becomes required. +default_datasource_uid = + [recording_rules] # Enable recording rules. enabled = true @@ -1747,9 +1759,8 @@ enabled = true #################################### Short Links ############################# [short_links] -# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is 7 days. Maximum value is 365. -# 0 means they will be deleted approximately every 10 minutes. A negative value (such as -1) will disable expiration. -expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +expire_time = -1 #################################### Internal Grafana Metrics ############ # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId @@ -2258,7 +2269,7 @@ fail_tests_on_console = true # List of targets that can be controlled by a repository, separated by |. # Instance means the whole grafana instance will be controlled by a repository. # Folder limits it to a folder within the grafana instance. -allowed_targets = instance|folder +allowed_targets = folder # Whether image rendering is allowed for dashboard previews. # Requires image rendering service to be configured. diff --git a/conf/sample.ini b/conf/sample.ini index d6397f894e4..530b14c87ac 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -322,6 +322,9 @@ # Rudderstack SDK url, optional, only valid if rudderstack_write_key and rudderstack_data_plane_url is also set ;rudderstack_sdk_url = +# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 +;rudderstack_v3_sdk_url = + # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config ;rudderstack_config_url = @@ -1612,6 +1615,11 @@ max_annotations_to_keep = # Accepts duration formats like: 30s, 1m, 1h. rule_query_offset = 1m +# Default data source UID to use for query execution when importing Prometheus rules. +# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided. +# If not set, the header becomes required. +default_datasource_uid = + #################################### Recording Rules ##################### [recording_rules] # Enable recording rules. @@ -1689,8 +1697,8 @@ default_datasource_uid = #################################### Short Links ############################# [short_links] -# Short links which are never accessed will be deleted as cleanup. Time is in days. Default is 7 days. Max is 365. 0 means they will be deleted approximately every 10 minutes. -;expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +;expire_time = -1 #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId diff --git a/devenv/dev-dashboards/migrations/migrations.json b/devenv/dev-dashboards/migrations/migrations.json index 4c2160e1f26..ad063b99cf4 100644 --- a/devenv/dev-dashboards/migrations/migrations.json +++ b/devenv/dev-dashboards/migrations/migrations.json @@ -299,15 +299,9 @@ "y": 0 }, "id": 6, - "options": { - "code": { - "language": "plaintext", - "showLineNumbers": false, - "showMiniMap": false - }, - "content": "# Graph panel >> Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", - "mode": "markdown" - }, + "options": {}, + "content": "# Graph panel >> Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", + "mode": "markdown", "pluginVersion": "11.0.0-pre", "targets": [ { diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests.json b/devenv/dev-dashboards/panel-gauge/gauge_tests.json index 0c8e2651e67..f32ace420b4 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests.json @@ -600,6 +600,20 @@ "stringInput": "null,null" } ], + "transformations": [ + { + "id": "convertFieldType", + "options": { + "fields": {}, + "conversions": [ + { + "targetField": "A-series", + "destinationType": "number" + } + ] + } + } + ], "title": "Only nulls and no user set min & max", "type": "gauge" }, diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 9cb204da48a..ff69226fdf5 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -71,14 +71,13 @@ "id": 1, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": false, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -148,14 +147,13 @@ "id": 4, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": false, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -225,14 +223,13 @@ "id": 3, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -299,94 +296,16 @@ "x": 12, "y": 1 }, - "id": 5, - "maxDataPoints": 20, - "options": { - "barWidthFactor": 0.4, - "effects": { - "barGlow": true, - "centerGlow": true, - "rounded": true, - "spotlight": true - }, - "gradient": "none", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "segmentCount": 1, - "segmentSpacing": 0.3, - "shape": "circle", - "showThresholdLabels": false, - "showThresholdMarkers": false, - "sparkline": false - }, - "pluginVersion": "13.0.0-pre", - "targets": [ - { - "alias": "1", - "datasource": { - "type": "grafana-testdata-datasource" - }, - "max": 100, - "min": 1, - "noise": 22, - "refId": "A", - "scenarioId": "random_walk", - "spread": 22, - "startValue": 1 - } - ], - "title": "Spotlight", - "type": "radialbar" - }, - { - "datasource": { - "type": "grafana-testdata-datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 16, - "y": 1 - }, "id": 8, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -450,20 +369,19 @@ "gridPos": { "h": 6, "w": 4, - "x": 0, - "y": 7 + "x": 16, + "y": 1 }, "id": 22, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -527,20 +445,19 @@ "gridPos": { "h": 6, "w": 4, - "x": 4, - "y": 7 + "x": 20, + "y": 1 }, "id": 23, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": true, "centerGlow": true, - "rounded": false, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -579,7 +496,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 7 }, "id": 17, "panels": [], @@ -616,21 +533,20 @@ }, "gridPos": { "h": 6, - "w": 5, + "w": 4, "x": 0, - "y": 14 + "y": 8 }, "id": 18, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.1, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -693,21 +609,20 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 5, - "y": 14 + "w": 4, + "x": 4, + "y": 8 }, "id": 19, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.32, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -770,21 +685,20 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 10, - "y": 14 + "w": 4, + "x": 8, + "y": 8 }, "id": 20, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.57, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -847,21 +761,20 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 15, - "y": 14 + "w": 4, + "x": 12, + "y": 8 }, "id": 21, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidthFactor": 0.8, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -900,7 +813,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 14 }, "id": 24, "panels": [], @@ -941,21 +854,20 @@ }, "gridPos": { "h": 6, - "w": 6, + "w": 4, "x": 0, - "y": 21 + "y": 15 }, "id": 25, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1018,21 +930,20 @@ }, "gridPos": { "h": 6, - "w": 6, - "x": 6, - "y": 21 + "w": 4, + "x": 4, + "y": 15 }, "id": 26, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1095,21 +1006,20 @@ }, "gridPos": { "h": 6, - "w": 5, - "x": 12, - "y": 21 + "w": 4, + "x": 8, + "y": 15 }, "id": 29, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1171,22 +1081,21 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 0, - "y": 27 + "h": 6, + "w": 4, + "x": 12, + "y": 15 }, "id": 30, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1248,22 +1157,21 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 6, - "x": 6, - "y": 27 + "h": 6, + "w": 4, + "x": 16, + "y": 15 }, "id": 28, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.72, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1298,7 +1206,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 21 }, "id": 31, "panels": [], @@ -1345,19 +1253,18 @@ "h": 10, "w": 7, "x": 0, - "y": 35 + "y": 22 }, "id": 32, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1426,19 +1333,18 @@ "h": 10, "w": 7, "x": 7, - "y": 35 + "y": 22 }, "id": 34, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1507,19 +1413,18 @@ "h": 10, "w": 6, "x": 14, - "y": 35 + "y": 22 }, "id": 33, "maxDataPoints": 20, "options": { + "barShape": "flat", "barWidthFactor": 0.9, "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1554,7 +1459,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 32 }, "id": 6, "panels": [], @@ -1565,7 +1470,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1596,21 +1500,21 @@ "h": 6, "w": 24, "x": 0, - "y": 46 + "y": 33 }, "id": 9, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, + "endpointMarker": "glow", "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1622,8 +1526,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1631,7 +1534,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1649,7 +1551,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1680,21 +1581,20 @@ "h": 6, "w": 24, "x": 0, - "y": 52 + "y": 39 }, "id": 11, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.4, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1706,8 +1606,7 @@ "shape": "gauge", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": true, - "spotlight": true + "sparkline": true }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1715,7 +1614,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 98, "min": 5, "noise": 22, @@ -1735,7 +1633,7 @@ "h": 1, "w": 24, "x": 0, - "y": 58 + "y": 45 }, "id": 12, "panels": [], @@ -1746,7 +1644,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1778,21 +1675,20 @@ "h": 7, "w": 4, "x": 0, - "y": 59 + "y": 46 }, "id": 13, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1804,8 +1700,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1813,7 +1708,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 8, "min": 1, "noise": 2, @@ -1824,6 +1718,22 @@ "startValue": 0 } ], + "transformations": [ + { + "id": "calculateField", + "options": { + "mode": "unary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true, + "unary": { + "operator": "round", + "fieldName": "A-series" + } + } + } + ], "title": "Active gateways", "type": "radialbar" }, @@ -1831,7 +1741,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1863,22 +1772,20 @@ "h": 7, "w": 5, "x": 4, - "y": 59 + "y": 46 }, "id": 14, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.49, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "sparkline": false, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1890,8 +1797,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1899,7 +1805,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 12, "min": 1, "noise": 2, @@ -1910,6 +1815,22 @@ "startValue": 0 } ], + "transformations": [ + { + "id": "calculateField", + "options": { + "mode": "unary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true, + "unary": { + "operator": "round", + "fieldName": "A-series" + } + } + } + ], "title": "Active pods", "type": "radialbar" }, @@ -1917,7 +1838,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -1948,21 +1868,20 @@ "h": 7, "w": 5, "x": 9, - "y": 59 + "y": 46 }, "id": 15, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.84, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1974,8 +1893,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -1983,7 +1901,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2001,7 +1918,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2032,21 +1948,20 @@ "h": 7, "w": 6, "x": 14, - "y": 59 + "y": 46 }, "id": 16, "maxDataPoints": 20, "options": { + "barShape": "rounded", "barWidth": 12, "barWidthFactor": 0.66, "effects": { "barGlow": true, "centerGlow": true, - "rounded": true, - "spotlight": true + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2058,8 +1973,7 @@ "shape": "circle", "showThresholdLabels": false, "showThresholdMarkers": false, - "sparkline": false, - "spotlight": true + "sparkline": false }, "pluginVersion": "13.0.0-pre", "targets": [ @@ -2067,7 +1981,6 @@ "datasource": { "type": "grafana-testdata-datasource" }, - "hide": false, "max": 100, "min": 10, "noise": 22, @@ -2080,6 +1993,145 @@ ], "title": "Backend", "type": "radialbar" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 35, + "panels": [], + "title": "Empty data", + "type": "row" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 36, + "options": { + "barShape": "flat", + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 0 + } + ], + "title": "Numeric, no series", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 37, + "options": { + "barShape": "flat", + "barWidthFactor": 0.5, + "effects": { + "barGlow": false, + "centerGlow": false, + "gradient": true + }, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "segmentCount": 1, + "segmentSpacing": 0.3, + "shape": "gauge", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sparkline": true + }, + "pluginVersion": "13.0.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "logs" + } + ], + "title": "Non-numeric", + "type": "gauge" } ], "preload": false, @@ -2096,5 +2148,6 @@ "timezone": "browser", "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", - "version": 6 + "version": 22, + "weekStart": "" } diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json index 61bec02a59b..3ca00ad5b63 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json @@ -474,6 +474,7 @@ }, "id": 12, "options": { + "displayName": "My gauge", "minVizHeight": 75, "minVizWidth": 75, "orientation": "auto", @@ -947,7 +948,6 @@ }, "orientation": "auto", "shape": "gauge", - "gradient": "none", "barWidthFactor": 0.5, "segmentCount": 70, "segmentSpacing": 0.3, @@ -957,8 +957,7 @@ "effects": { "barGlow": false, "centerGlow": false, - "rounded": false, - "spotlight": false + "gradient": false } } }, diff --git a/devenv/docker/blocks/mysql/docker-compose.yaml b/devenv/docker/blocks/mysql/docker-compose.yaml index cd5de0e6e67..c0c5403ef25 100644 --- a/devenv/docker/blocks/mysql/docker-compose.yaml +++ b/devenv/docker/blocks/mysql/docker-compose.yaml @@ -7,7 +7,15 @@ MYSQL_PASSWORD: password ports: - "3306:3306" - command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all, --default-authentication-plugin=mysql_native_password] + command: + - mysqld + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --innodb_monitor_enable=all + - --default-authentication-plugin=mysql_native_password + # Please keep sql-require-primary-key option enabled, to make sure we don't accidentally introduce migration + # adding new table without PK. + - --sql-require-primary-key=ON fake-mysql-data: image: grafana/fake-data-gen diff --git a/devenv/scopes/scopes-config.yaml b/devenv/scopes/scopes-config.yaml index 54842e5818e..ef800415547 100644 --- a/devenv/scopes/scopes-config.yaml +++ b/devenv/scopes/scopes-config.yaml @@ -83,6 +83,12 @@ tree: nodeType: leaf linkId: test-case-2 linkType: scope + test-case-redirect: + title: Test case with redirect + nodeType: leaf + linkId: shoe-org + linkType: scope + redirectPath: /d/dcb9f5e9-8066-4397-889e-864b99555dbb #Reliability dashboard clusters: title: Clusters nodeType: container @@ -119,6 +125,7 @@ navigationTree: url: /d/_5rDmaQiz scope: shoe-org subScope: shoes + preLoadSubScopeChildren: true children: - name: shoes-overview title: Overview @@ -135,6 +142,7 @@ navigationTree: url: /d/edediimbjhdz4b scope: shoes subScope: frontend + preLoadSubScopeChildren: true children: - name: frontend-api title: API Metrics @@ -204,6 +212,7 @@ navigationTree: url: /d/UTv--wqMk scope: shoe-org subScope: apparel + disableSubScopeSelection: true children: - name: apparel-product-overview title: Product Overview diff --git a/devenv/scopes/scopes.go b/devenv/scopes/scopes.go index 1f8a698ccda..e3f4de80d21 100644 --- a/devenv/scopes/scopes.go +++ b/devenv/scopes/scopes.go @@ -67,30 +67,36 @@ type ScopeFilterConfig struct { type TreeNode struct { Title string `yaml:"title"` SubTitle string `yaml:"subTitle,omitempty"` + Description string `yaml:"description,omitempty"` NodeType string `yaml:"nodeType"` LinkID string `yaml:"linkId,omitempty"` LinkType string `yaml:"linkType,omitempty"` DisableMultiSelect bool `yaml:"disableMultiSelect,omitempty"` + RedirectPath string `yaml:"redirectPath,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"` // Required scope - SubScope string `yaml:"subScope"` // Optional subScope for hierarchical navigation - Title string `yaml:"title"` // Display title - Groups []string `yaml:"groups"` // Optional groups for categorization + URL string `yaml:"url"` // URL path (e.g., /d/abc123 or /explore) + Scope string `yaml:"scope"` // Required scope + SubScope string `yaml:"subScope"` // Optional subScope for hierarchical navigation + Title string `yaml:"title"` // Display title + Groups []string `yaml:"groups"` // Optional groups for categorization + DisableSubScopeSelection bool `yaml:"disableSubScopeSelection"` // Makes the subscope not selectable + PreLoadSubScopeChildren bool `yaml:"preLoadSubScopeChildren"` // Preload children of subScope without updating UI } // NavigationTreeNode represents a node in the navigation tree structure type NavigationTreeNode struct { - Name string `yaml:"name"` - Title string `yaml:"title"` - URL string `yaml:"url"` - Scope string `yaml:"scope"` - SubScope string `yaml:"subScope,omitempty"` - Groups []string `yaml:"groups,omitempty"` - Children []NavigationTreeNode `yaml:"children,omitempty"` + Name string `yaml:"name"` + Title string `yaml:"title"` + URL string `yaml:"url"` + Scope string `yaml:"scope"` + SubScope string `yaml:"subScope,omitempty"` + Groups []string `yaml:"groups,omitempty"` + DisableSubScopeSelection bool `yaml:"disableSubScopeSelection,omitempty"` + PreLoadSubScopeChildren bool `yaml:"preLoadSubScopeChildren,omitempty"` // Preload children of subScope without updating UI + Children []NavigationTreeNode `yaml:"children,omitempty"` } // Helper function to convert ScopeFilterConfig to v0alpha1.ScopeFilter @@ -259,6 +265,7 @@ func (c *Client) createScopeNode(name string, node TreeNode, parentName string) spec := v0alpha1.ScopeNodeSpec{ Title: node.Title, SubTitle: node.SubTitle, + Description: node.Description, NodeType: nodeType, DisableMultiSelect: node.DisableMultiSelect, } @@ -272,6 +279,10 @@ func (c *Client) createScopeNode(name string, node TreeNode, parentName string) spec.LinkType = linkType } + if node.RedirectPath != "" { + spec.RedirectPath = node.RedirectPath + } + resource := v0alpha1.ScopeNode{ TypeMeta: metav1.TypeMeta{ APIVersion: apiVersion, @@ -306,8 +317,10 @@ func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error prefixedScope := prefix + "-" + nav.Scope spec := v0alpha1.ScopeNavigationSpec{ - URL: nav.URL, - Scope: prefixedScope, + URL: nav.URL, + Scope: prefixedScope, + DisableSubScopeSelection: nav.DisableSubScopeSelection, + PreLoadSubScopeChildren: nav.PreLoadSubScopeChildren, } if nav.SubScope != "" { @@ -343,14 +356,14 @@ func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error return err } + // Get the created resource to retrieve its resourceVersion for status update + createdNav, err := c.getScopeNavigation(prefixedName) + if err != nil { + return fmt.Errorf("failed to get created navigation: %w", err) + } + // Update status in a second request (status is a subresource) if nav.Title != "" || len(nav.Groups) > 0 { - // Get the created resource to retrieve its resourceVersion and existing spec - createdNav, err := c.getScopeNavigation(prefixedName) - if err != nil { - return fmt.Errorf("failed to get created navigation: %w", err) - } - statusResource := v0alpha1.ScopeNavigation{ TypeMeta: metav1.TypeMeta{ APIVersion: apiVersion, @@ -397,9 +410,11 @@ func treeToNavigations(node NavigationTreeNode, parentPath []string, dashboardCo // Create navigation for this node nav := NavigationConfig{ - URL: url, - Scope: node.Scope, - Title: node.Title, + URL: url, + Scope: node.Scope, + Title: node.Title, + DisableSubScopeSelection: node.DisableSubScopeSelection, + PreLoadSubScopeChildren: node.PreLoadSubScopeChildren, } if node.SubScope != "" { nav.SubScope = node.SubScope diff --git a/docs/Makefile b/docs/Makefile index 7bdbe026293..8202b11f826 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,8 +7,8 @@ MAKEFLAGS += --no-builtin-rule include docs.mk -.PHONY: sources/panels-visualizations/query-transform-data/transform-data/index.md -sources/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source. +.PHONY: sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md +sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source. cd $(CURDIR)/.. && \ npx tsx ./scripts/docs/generate-transformations.ts && \ npx prettier -w $(CURDIR)/$@ diff --git a/docs/sources/administration/plugin-management/plugin-install.md b/docs/sources/administration/plugin-management/plugin-install.md index 18be4ea58fa..dfec5002944 100644 --- a/docs/sources/administration/plugin-management/plugin-install.md +++ b/docs/sources/administration/plugin-management/plugin-install.md @@ -21,11 +21,28 @@ weight: 120 # Install a plugin -Besides the UI, you can use alternative methods to install a plugin depending on your environment or set-up. +{{< admonition type="note" >}} + +Installing plugins from the Grafana website into a Grafana Cloud instance will be removed in February 2026. + +If you're a Grafana Cloud user, follow [Install a plugin through the Grafana UI](#install-a-plugin-through-the-grafana-uiinstall-a-plugin-through-the-grafana-ui) instead. + +{{< /admonition >}} + +## Install a plugin through the Grafana UI + +The most common way to install a plugin is through the Grafana UI. + +1. In Grafana, click **Administration > Plugins and data > Plugins** in the side navigation menu to view all plugins. +1. Browse and find a plugin. +1. Click the plugin's logo. +1. Click **Install**. + +You can use use the following alternative methods to install a plugin depending on your environment or setup. ## Install a plugin using Grafana CLI -The Grafana CLI allows you to install, upgrade, and manage your Grafana plugins using a command line tool. For more information about Grafana CLI plugin commands, refer to [Plugin commands](/docs/grafana//cli/#plugins-commands). +The Grafana CLI allows you to install, upgrade, and manage your Grafana plugins using a command line tool. For more information about Grafana CLI plugin commands, refer to [Plugin commands](https://grafana.com/docs/grafana//administration/cli/#plugins-commands). ## Install a plugin from a ZIP file diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index 641d9bb4180..5fb3705232e 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -238,6 +238,7 @@ For more information on Cloud Access Policies and how to use them, see [Access p | `alert.notifications.templates:read` | None | Read templates. | | `alert.notifications.templates:write` | None | Create new or update existing templates. | | `alert.notifications.templates:delete` | None | Delete existing templates. | +| `alert.notifications.templates.test:write` | None | Test templates with custom payloads (preview and payload editor functionality). | | `alert.notifications.routes:read` | None | Read notification policies. | | `alert.notifications.routes:write` | None | Create new, update or delete notification policies | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 43b6e9c74a1..82ebfa0c49a 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -44,7 +44,7 @@ refs: destination: /docs/grafana-cloud/alerting-and-irm/oncall/user-and-team-management/#available-grafana-oncall-rbac-roles--granted-actions --- -# RBAC role definitions +# Grafana RBAC role definitions {{< admonition type="note" >}} Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud). @@ -59,7 +59,7 @@ The following tables list permissions associated with basic and fixed roles. Thi | Grafana Admin | `basic_grafana_admin` | | `fixed:authentication.config:writer`
`fixed:general.auth.config:writer`
`fixed:ldap:writer`
`fixed:licensing:writer`
`fixed:migrationassistant:migrator`
`fixed:org.users:writer`
`fixed:organization:maintainer`
`fixed:plugins:maintainer`
`fixed:provisioning:writer`
`fixed:roles:writer`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:stats:reader`
`fixed:support.bundles:writer`
`fixed:usagestats:reader`
`fixed:users:writer` | Default [Grafana server administrator](/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) assignments. | | Admin | `basic_admin` | All roles assigned to Editor and `fixed:reports:writer`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:writer`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:writer`
`fixed:plugins:writer`
`fixed:library.panels:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | -| Editor | `basic_editor` | All roles assigned to Viewer and `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:alerting:writer`
`fixed:library.panels:creator`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | +| Editor | `basic_editor` | All roles assigned to Viewer and `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:alerting:writer`
`fixed:library.panels:creator`
`fixed:library.panels:general.writer`
`fixed:alerting.provisioning.provenance:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | | Viewer | `basic_viewer` | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:library.panels:general.reader`
`fixed:folders.general:reader`
`fixed:datasources.builtin:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | | No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) | @@ -74,86 +74,86 @@ These UUIDs won't be available if your instance was created before Grafana v10.2 To learn how to use the roles API to determine the role UUIDs, refer to [Manage RBAC roles](ref:rbac-manage-rbac-roles). {{< /admonition >}} -| Fixed role | UUID | Permissions | Description | -| -------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fixed:alerting:reader` | `fixed_O2oP1_uBFozI2i93klAkcvEWR30` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting:writer` | `fixed_-PAZgSJsDlRD8NUg-PFSeH_BkJY` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.instances:reader` | `fixed_ut5fVS-Ulh_ejFoskFhJT_rYg0Y` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | -| `fixed:alerting.instances:writer` | `fixed_pKOBJE346uyqMLdgWbk1NsQfEl0` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | -| `fixed:alerting.notifications:reader` | `fixed_hmBn0lX5h1RZXB9Vaot420EEdA0` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.notifications:writer` | `fixed_XplK6HPNxf9AP5IGTdB5Iun4tJc` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | -| `fixed:alerting.provisioning:writer` | `fixed_y7pFjdEkxpx5ETdcxPvp0AgRuUo` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | -| `fixed:alerting.provisioning.secrets:reader` | `fixed_9fmzXXZZG-Od0Amy2ofEG8Uk--c` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) | -| `fixed:alerting.provisioning.status:writer` | `fixed_eAxlzfkTuobvKEgXHveFMBZrOj8` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) | -| `fixed:alerting.rules:reader` | `fixed_fRGKL_vAqUsmUWq5EYKnOha9DcA` | `alert.rule:read`, `alert.silences:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences | -| `fixed:alerting.rules:writer` | `fixed_YJJGwAalUwDZPrXSyFH8GfYBXAc` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences | -| `fixed:annotations:reader` | `fixed_hpZnoizrfAJsrceNcNQqWYV-xNU` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | -| `fixed:annotations:writer` | `fixed_ZVW-Aa9Tzle6J4s2aUFcq1StKWE` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | -| `fixed:annotations.dashboard:writer` | `fixed_8A775xenXeKaJk4Cr7bchP9yXOA` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | -| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | -| `fixed:general.auth.config:writer` | `fixed_QFxIT_FGtBqbIVJIwx1bLgI5z6c` | `settings:read` for scope `settings:auth:oauth_allow_insecure_email_lookup`
`settings:write` for scope `settings:auth:oauth_allow_insecure_email_lookup` | Read and update the Grafana instance's general authentication configuration settings. | -| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`
`folders:read` | Create dashboards. | -| `fixed:dashboards:reader` | `fixed_Sgr67JTOhjQGFlzYRahOe45TdWM` | `dashboards:read` | Read all dashboards. | -| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | -| `fixed:dashboards.insights:reader` | `fixed_JlBJ2_gizP8zhgaeGE2rjyZe2Rs` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | -| `fixed:dashboards.permissions:reader` | `fixed_f17oxuXW_58LL8mYJsm4T_mCeIw` | `dashboards.permissions:read` | Read all dashboard permissions. | -| `fixed:dashboards.permissions:writer` | `fixed_CcznxhWX_Yqn8uWMXMQ-b5iFW9k` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | -| `fixed:dashboards.public:writer` | `fixed_f_GHHRBciaqESXfGz2oCcooqHxs` | `dashboards.public:write` | Create, update, delete or pause a shared dashboard. | -| `fixed:datasources:creator` | `fixed_XX8jHREgUt-wo1A-rPXIiFlX6Zw` | `datasources:create` | Create data sources. | -| `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | -| `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`
`datasources:query` | Read and query data sources. | -| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | -| `fixed:datasources.builtin:reader` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | `datasources:read` and `datasources:query` scoped to `datasources:uid:grafana` | An internal role used to grant Viewers access to the builtin example data source in Grafana. | -| `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. | -| `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | -| `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. | -| `fixed:datasources.insights:reader` | `fixed_EBZ3NwlfecNPp2p0XcZRC1nfEYk` | `datasources.insights:read` | Read data source insights data. | -| `fixed:datasources.permissions:reader` | `fixed_ErYA-cTN3yn4h4GxaVPcawRhiOY` | `datasources.permissions:read` | Read data source permissions. | -| `fixed:datasources.permissions:writer` | `fixed_aiQh9YDfLOKjQhYasF9_SFUjQiw` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | -| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. | -| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | -| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, update, and delete all folders and dashboards. Create folders and subfolders. | -| `fixed:folders.general:reader` | `fixed_rSASbkg8DvpG_gTX5s41d7uxRvI` | `folders:read` scoped to `folders:uid:general` | An internal role used to correctly display access to the folder tree for Viewer role. | -| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. | -| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | -| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | -| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`
`folders:read` | Create library panel at the root level. | -| `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. | -| `fixed:library.panels:general.writer` | `fixed_DgprkmqfN_1EhZ2v1_d1fYG8LzI` | All permissions from `fixed:library.panels:general.reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. | -| `fixed:library.panels:reader` | `fixed_tvTr9CnZ6La5vvUO_U_X1LPnhUs` | `library.panels:read` | Read all library panels. | -| `fixed:library.panels:writer` | `fixed_JTljAr21LWLTXCkgfBC4H0lhBC8` | All permissions from `fixed:library.panels:reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions. | -| `fixed:licensing:reader` | `fixed_OADpuXvNEylO2Kelu3GIuBXEAYE` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | -| `fixed:licensing:writer` | `fixed_gzbz3rJpQMdaKHt-E4q0PVaKMoE` | All permissions from `fixed:licensing:reader` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | -| `fixed:migrationassistant:migrator` | `fixed_LLk2p7TRuBztOAksTQb1Klc8YTk` | `migrationassistant:migrate` | Execute on-prem to cloud migrations through the Migration Assistant. | -| `fixed:org.users:reader` | `fixed_oCqNwlVHLOpw7-jAlwp4HzYqwGY` | `org.users:read` | Read users within a single organization. | -| `fixed:org.users:writer` | `fixed_VERj5nayasjgf_Yh0sWqqCkxWlw` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | -| `fixed:organization:maintainer` | `fixed_CMm-uuBaPUBf4r8XG3jIvxo55bg` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | -| `fixed:organization:reader` | `fixed_0SZPJlTHdNEe8zO91zv7Zwiwa2w` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | -| `fixed:organization:writer` | `fixed_Y4jGqDd8w1yCrPwlik8z5Iu8-3M` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | -| `fixed:plugins:maintainer` | `fixed_yEOKidBcWgbm74x-nTa3lW5lOyY` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. | -| `fixed:plugins:writer` | `fixed_MRYpGk7kpNNwt2VoVOXFiPnQziE` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | -| `fixed:plugins.app:reader` | `fixed_AcZRiNYx7NueYkUqzw1o2OGGUAA` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | -| `fixed:provisioning:writer` | `fixed_bgk1FCyR6OEDwhgirZlQgu5LlCA` | `provisioning:reload` | Reload provisioning. | -| `fixed:reports:reader` | `fixed_72_8LU_0ukfm6BdblOw8Z9q-GQ8` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | -| `fixed:reports:writer` | `fixed_jBW3_7g1EWOjGVBYeVRwtFxhUNw` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | -| `fixed:roles:reader` | `fixed_GkfG-1NSwEGb4hpK3-E3qHyNltc` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | -| `fixed:roles:resetter` | `fixed_WgPpC3qJRmVpVTJavFNwfS5RuzQ` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | -| `fixed:roles:writer` | `fixed_W5aFaw8isAM27x_eWfElBhZ0iOc` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | -| `fixed:serviceaccounts:creator` | `fixed_Ikw60fckA0MyiiZ73BawSfOULy4` | `serviceaccounts:create` | Create Grafana service accounts. | -| `fixed:serviceaccounts:reader` | `fixed_QFjJAZ88iawMLInYOxPA1DB1w6I` | `serviceaccounts:read` | Read Grafana service accounts. | -| `fixed:serviceaccounts:writer` | `fixed_iBvUNUEZBZ7PUW0vdkN5iojc2sk` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | -| `fixed:settings:reader` | `fixed_0LaUt1x6PP8hsZzEBhqPQZFUd8Q` | `settings:read` | Read Grafana instance settings. | -| `fixed:settings:writer` | `fixed_joIHDgMrGg790hMhUufVzcU4j44` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | -| `fixed:stats:reader` | `fixed_OnRCXxZVINWpcKvTF5A1gecJ7pA` | `server.stats:read` | Read Grafana instance statistics. | -| `fixed:support.bundles:reader` | `fixed_gcPjI3PTUJwRx-GJZwDhNa7zbos` | `support.bundles:read` | List and download support bundles. | -| `fixed:support.bundles:writer` | `fixed_dTgCv9Wxrp_WHAhwHYIgeboxKpE` | `support.bundles:read`
`support.bundles:create`
`support.bundles:delete` | Create, delete, list and download support bundles. | -| `fixed:teams:creator` | `fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | -| `fixed:teams:read` | `fixed_Z8pB0GQlrqRt8IZBCJQxPWvJPgQ` | `teams:read` | List all teams. | -| `fixed:teams:writer` | `fixed_xw1T0579h620MOYi4L96GUs7fZY` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | -| `fixed:usagestats:reader` | `fixed_eAM0azEvnWFCJAjNkUKnGL_1-bU` | `server.usagestats.report:read` | View usage statistics report. | -| `fixed:users:reader` | `fixed_buZastUG3reWyQpPemcWjGqPAd0` | `users:read`
`users.quotas:read`
`users.authtoken:read` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | -| `fixed:users:writer` | `fixed_wjzgHHo_Ux25DJuELn_oiAdB_yM` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | +| Fixed role | UUID | Permissions | Description | +| ----------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fixed:alerting:reader` | `fixed_O2oP1_uBFozI2i93klAkcvEWR30` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting:writer` | `fixed_-PAZgSJsDlRD8NUg-PFSeH_BkJY` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.instances:reader` | `fixed_ut5fVS-Ulh_ejFoskFhJT_rYg0Y` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | +| `fixed:alerting.instances:writer` | `fixed_pKOBJE346uyqMLdgWbk1NsQfEl0` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | +| `fixed:alerting.notifications:reader` | `fixed_hmBn0lX5h1RZXB9Vaot420EEdA0` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.notifications:writer` | `fixed_XplK6HPNxf9AP5IGTdB5Iun4tJc` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | +| `fixed:alerting.provisioning:writer` | `fixed_y7pFjdEkxpx5ETdcxPvp0AgRuUo` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | +| `fixed:alerting.provisioning.secrets:reader` | `fixed_9fmzXXZZG-Od0Amy2ofEG8Uk--c` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) | +| `fixed:alerting.provisioning.provenance:writer` | `fixed_eAxlzfkTuobvKEgXHveFMBZrOj8` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) | +| `fixed:alerting.rules:reader` | `fixed_fRGKL_vAqUsmUWq5EYKnOha9DcA` | `alert.rule:read`, `alert.silences:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences | +| `fixed:alerting.rules:writer` | `fixed_YJJGwAalUwDZPrXSyFH8GfYBXAc` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences | +| `fixed:annotations:reader` | `fixed_hpZnoizrfAJsrceNcNQqWYV-xNU` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | +| `fixed:annotations:writer` | `fixed_ZVW-Aa9Tzle6J4s2aUFcq1StKWE` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | +| `fixed:annotations.dashboard:writer` | `fixed_8A775xenXeKaJk4Cr7bchP9yXOA` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | +| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | +| `fixed:general.auth.config:writer` | `fixed_QFxIT_FGtBqbIVJIwx1bLgI5z6c` | `settings:read` for scope `settings:auth:oauth_allow_insecure_email_lookup`
`settings:write` for scope `settings:auth:oauth_allow_insecure_email_lookup` | Read and update the Grafana instance's general authentication configuration settings. | +| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`
`folders:read` | Create dashboards. | +| `fixed:dashboards:reader` | `fixed_Sgr67JTOhjQGFlzYRahOe45TdWM` | `dashboards:read` | Read all dashboards. | +| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | +| `fixed:dashboards.insights:reader` | `fixed_JlBJ2_gizP8zhgaeGE2rjyZe2Rs` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | +| `fixed:dashboards.permissions:reader` | `fixed_f17oxuXW_58LL8mYJsm4T_mCeIw` | `dashboards.permissions:read` | Read all dashboard permissions. | +| `fixed:dashboards.permissions:writer` | `fixed_CcznxhWX_Yqn8uWMXMQ-b5iFW9k` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | +| `fixed:dashboards.public:writer` | `fixed_f_GHHRBciaqESXfGz2oCcooqHxs` | `dashboards.public:write` | Create, update, delete or pause a shared dashboard. | +| `fixed:datasources:creator` | `fixed_XX8jHREgUt-wo1A-rPXIiFlX6Zw` | `datasources:create` | Create data sources. | +| `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | +| `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`
`datasources:query` | Read and query data sources. | +| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | +| `fixed:datasources.builtin:reader` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | `datasources:read` and `datasources:query` scoped to `datasources:uid:grafana` | An internal role used to grant Viewers access to the builtin example data source in Grafana. | +| `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. | +| `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | +| `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. | +| `fixed:datasources.insights:reader` | `fixed_EBZ3NwlfecNPp2p0XcZRC1nfEYk` | `datasources.insights:read` | Read data source insights data. | +| `fixed:datasources.permissions:reader` | `fixed_ErYA-cTN3yn4h4GxaVPcawRhiOY` | `datasources.permissions:read` | Read data source permissions. | +| `fixed:datasources.permissions:writer` | `fixed_aiQh9YDfLOKjQhYasF9_SFUjQiw` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | +| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. | +| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | +| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, update, and delete all folders and dashboards. Create folders and subfolders. | +| `fixed:folders.general:reader` | `fixed_rSASbkg8DvpG_gTX5s41d7uxRvI` | `folders:read` scoped to `folders:uid:general` | An internal role used to correctly display access to the folder tree for Viewer role. | +| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. | +| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | +| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | +| `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | +| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`
`folders:read` | Create library panel at the root level. | +| `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. | +| `fixed:library.panels:general.writer` | `fixed_DgprkmqfN_1EhZ2v1_d1fYG8LzI` | All permissions from `fixed:library.panels:general.reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. | +| `fixed:library.panels:reader` | `fixed_tvTr9CnZ6La5vvUO_U_X1LPnhUs` | `library.panels:read` | Read all library panels. | +| `fixed:library.panels:writer` | `fixed_JTljAr21LWLTXCkgfBC4H0lhBC8` | All permissions from `fixed:library.panels:reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions. | +| `fixed:licensing:reader` | `fixed_OADpuXvNEylO2Kelu3GIuBXEAYE` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | +| `fixed:licensing:writer` | `fixed_gzbz3rJpQMdaKHt-E4q0PVaKMoE` | All permissions from `fixed:licensing:reader` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | +| `fixed:migrationassistant:migrator` | `fixed_LLk2p7TRuBztOAksTQb1Klc8YTk` | `migrationassistant:migrate` | Execute on-prem to cloud migrations through the Migration Assistant. | +| `fixed:org.users:reader` | `fixed_oCqNwlVHLOpw7-jAlwp4HzYqwGY` | `org.users:read` | Read users within a single organization. | +| `fixed:org.users:writer` | `fixed_VERj5nayasjgf_Yh0sWqqCkxWlw` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | +| `fixed:organization:maintainer` | `fixed_CMm-uuBaPUBf4r8XG3jIvxo55bg` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | +| `fixed:organization:reader` | `fixed_0SZPJlTHdNEe8zO91zv7Zwiwa2w` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | +| `fixed:organization:writer` | `fixed_Y4jGqDd8w1yCrPwlik8z5Iu8-3M` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | +| `fixed:plugins:maintainer` | `fixed_yEOKidBcWgbm74x-nTa3lW5lOyY` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. | +| `fixed:plugins:writer` | `fixed_MRYpGk7kpNNwt2VoVOXFiPnQziE` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | +| `fixed:plugins.app:reader` | `fixed_AcZRiNYx7NueYkUqzw1o2OGGUAA` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | +| `fixed:provisioning:writer` | `fixed_bgk1FCyR6OEDwhgirZlQgu5LlCA` | `provisioning:reload` | Reload provisioning. | +| `fixed:reports:reader` | `fixed_72_8LU_0ukfm6BdblOw8Z9q-GQ8` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | +| `fixed:reports:writer` | `fixed_jBW3_7g1EWOjGVBYeVRwtFxhUNw` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | +| `fixed:roles:reader` | `fixed_GkfG-1NSwEGb4hpK3-E3qHyNltc` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | +| `fixed:roles:resetter` | `fixed_WgPpC3qJRmVpVTJavFNwfS5RuzQ` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | +| `fixed:roles:writer` | `fixed_W5aFaw8isAM27x_eWfElBhZ0iOc` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | +| `fixed:serviceaccounts:creator` | `fixed_Ikw60fckA0MyiiZ73BawSfOULy4` | `serviceaccounts:create` | Create Grafana service accounts. | +| `fixed:serviceaccounts:reader` | `fixed_QFjJAZ88iawMLInYOxPA1DB1w6I` | `serviceaccounts:read` | Read Grafana service accounts. | +| `fixed:serviceaccounts:writer` | `fixed_iBvUNUEZBZ7PUW0vdkN5iojc2sk` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | +| `fixed:settings:reader` | `fixed_0LaUt1x6PP8hsZzEBhqPQZFUd8Q` | `settings:read` | Read Grafana instance settings. | +| `fixed:settings:writer` | `fixed_joIHDgMrGg790hMhUufVzcU4j44` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | +| `fixed:stats:reader` | `fixed_OnRCXxZVINWpcKvTF5A1gecJ7pA` | `server.stats:read` | Read Grafana instance statistics. | +| `fixed:support.bundles:reader` | `fixed_gcPjI3PTUJwRx-GJZwDhNa7zbos` | `support.bundles:read` | List and download support bundles. | +| `fixed:support.bundles:writer` | `fixed_dTgCv9Wxrp_WHAhwHYIgeboxKpE` | `support.bundles:read`
`support.bundles:create`
`support.bundles:delete` | Create, delete, list and download support bundles. | +| `fixed:teams:creator` | `fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | +| `fixed:teams:read` | `fixed_Z8pB0GQlrqRt8IZBCJQxPWvJPgQ` | `teams:read` | List all teams. | +| `fixed:teams:writer` | `fixed_xw1T0579h620MOYi4L96GUs7fZY` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | +| `fixed:usagestats:reader` | `fixed_eAM0azEvnWFCJAjNkUKnGL_1-bU` | `server.usagestats.report:read` | View usage statistics report. | +| `fixed:users:reader` | `fixed_buZastUG3reWyQpPemcWjGqPAd0` | `users:read`
`users.quotas:read`
`users.authtoken:read` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | +| `fixed:users:writer` | `fixed_wjzgHHo_Ux25DJuELn_oiAdB_yM` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | ### Alerting roles @@ -164,10 +164,20 @@ Access to Grafana alert rules is an intersection of many permissions: - Permission to read a folder. For example, the fixed role `fixed:folders:reader` includes the action `folders:read` and a folder scope `folders:id:`. - Permission to query **all** data sources that a given alert rule uses. If a user cannot query a given data source, they cannot see any alert rules that query that data source. -There is only one exclusion at this moment. Role `fixed:alerting.provisioning:writer` does not require user to have any additional permissions and provides access to all aspects of the alerting configuration via special provisioning API. +There is only one exclusion. Role `fixed:alerting.provisioning:writer` does not require user to have any additional permissions and provides access to all aspects of the alerting configuration via special provisioning API. For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder](ref:plan-rbac-rollout-strategy-create-a-custom-role-to-access-alerts-in-a-folder). +#### Alerting basic roles + +The following table lists the default RBAC alerting role assignments to the basic roles: + +| Basic role | Associated fixed roles | Description | +| ---------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Admin | `fixed:alerting:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. | +| Editor | `fixed:alerting:writer`
`fixed:alerting.provisioning.provenance:writer` | Default [Editor](ref:rbac-basic-roles) assignments. | +| Viewer | `fixed:alerting:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. | + ### Grafana OnCall roles If you are using [Grafana OnCall](ref:oncall), you can try out the integration between Grafana OnCall and RBAC. diff --git a/docs/sources/alerting/alerting-rules/alerting-migration.md b/docs/sources/alerting/alerting-rules/alerting-migration.md index 3afa3aec453..320b04b4643 100644 --- a/docs/sources/alerting/alerting-rules/alerting-migration.md +++ b/docs/sources/alerting/alerting-rules/alerting-migration.md @@ -134,7 +134,7 @@ To convert data source-managed alert rules to Grafana managed alerts: Pausing stops alert rule evaluation behavior for the newly created Grafana-managed alert rules. -9. (Optional) In the **Target data source** of the **Recording rules** section, you can select the data source that the imported recording rules will query. By default, it is the data source selected in the **Data source** dropdown. +9. (Optional) In the **Target data source** of the **Recording rules** section, you can select the data source to which the imported recording rules will write metrics. By default, it is the data source selected in the **Data source** dropdown. 10. Click **Import**. @@ -242,6 +242,8 @@ Set to `true` to import recording rules in paused state. The UID of the data source to use for alert rule queries. +If not specified in the header, Grafana uses the configured default from `unified_alerting.prometheus_conversion.default_datasource_uid`. If neither the header nor the configuration option is provided, the request fails. + #### `X-Grafana-Alerting-Target-Datasource-UID` The UID of the target data source for recording rules. If not specified, the value from `X-Grafana-Alerting-Datasource-UID` is used. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-alertmanager.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-alertmanager.md index 4cdc84ec366..f34a9eeb654 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-alertmanager.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-alertmanager.md @@ -59,9 +59,9 @@ For more details on contact points, including how to test them and enable notifi ## Alertmanager settings -| Option | Description | -| ------ | ---------------------------------------------------------------------------------------------------------------------------------- | -| URL | The Alertmanager URL. This field is [protected](ref:configure-contact-points#protected-fields) from modification in Grafana Cloud. | +| Option | Description | +| ------ | ----------------------------------------------------------------------------------------------------------------- | +| URL | The Alertmanager URL. This field is [protected](ref:configure-contact-points) from modification in Grafana Cloud. | #### Optional settings diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md index 659341405f1..9f68f70ae08 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md @@ -49,14 +49,14 @@ For more details on contact points, including how to test them and enable notifi ### Required Settings -| Key | Description | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| URL | The URL of the REST API of your Jira instance. Supported versions: `2` and `3` (e.g., `https://your-domain.atlassian.net/rest/api/3`). This field is [protected](ref:configure-contact-points#protected-fields) from modification in Grafana Cloud. | -| Basic Auth User | Username for authentication. For Jira Cloud, use your email address. | -| Basic Auth Password | Password or personal token. For Jira Cloud, you need to obtain a personal token [here](https://id.atlassian.com/manage-profile/security/api-tokens) and use it as the password. | -| API Token | An alternative to basic authentication, a bearer token is used to authorize the API requests. See [Jira documentation](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) for more information. | -| Project Key | The project key identifying the project where issues will be created. Project keys are unique identifiers for a project. | -| Issue Type | The type of issue to create (e.g., `Task`, `Bug`, `Incident`). Make sure that you specify a type that is available in your project. | +| Key | Description | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| URL | The URL of the REST API of your Jira instance. Supported versions: `2` and `3` (e.g., `https://your-domain.atlassian.net/rest/api/3`). This field is [protected](ref:configure-contact-points) from modification in Grafana Cloud. | +| Basic Auth User | Username for authentication. For Jira Cloud, use your email address. | +| Basic Auth Password | Password or personal token. For Jira Cloud, you need to obtain a personal token [here](https://id.atlassian.com/manage-profile/security/api-tokens) and use it as the password. | +| API Token | An alternative to basic authentication, a bearer token is used to authorize the API requests. See [Jira documentation](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) for more information. | +| Project Key | The project key identifying the project where issues will be created. Project keys are unique identifiers for a project. | +| Issue Type | The type of issue to create (e.g., `Task`, `Bug`, `Incident`). Make sure that you specify a type that is available in your project. | ### Optional Settings diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md index 6f76a574619..3f6a403d27a 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md @@ -54,10 +54,10 @@ For more details on contact points, including how to test them and enable notifi ### Required Settings -| Option | Description | -| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| Broker URL | The URL of the MQTT broker. This field is [protected](ref:configure-contact-points#protected-fields) from modification in Grafana Cloud. | -| Topic | The topic to which the message will be sent. | +| Option | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------- | +| Broker URL | The URL of the MQTT broker. This field is [protected](ref:configure-contact-points) from modification in Grafana Cloud. | +| Topic | The topic to which the message will be sent. | ### Optional Settings diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md index 8f56174ed60..3bda324ec23 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md @@ -51,8 +51,8 @@ You can customize the `title` and `body` of the Slack message using [notificatio If you are using a Slack API Token, complete the following steps. -1. Follow steps 1 and 2 of the [Slack API Quickstart](https://api.slack.com/start/quickstart). -1. Add the [chat:write.public](https://api.slack.com/scopes/chat:write.public) scope to give your app the ability to post in all public channels without joining. +1. Follow step 1 of the [Slack API Quickstart](https://docs.slack.dev/app-management/quickstart-app-settings/#creating) to create the app. +1. Continue onto the second step of the [Slack API Quickstart](https://docs.slack.dev/app-management/quickstart-app-settings/#scopes) and add the [chat:write.public](https://api.slack.com/scopes/chat:write.public) scope as described to give your app the ability to post in all public channels without joining. 1. In OAuth Tokens for Your Workspace, copy the Bot User OAuth Token. 1. Open your Slack workplace. 1. Right click the channel you want to receive notifications in. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index 120a45be2a2..d24b54fc568 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -62,9 +62,9 @@ For more details on contact points, including how to test them and enable notifi ## Webhook settings -| Option | Description | -| ------ | ----------------------------------------------------------------------------------------------------------------------------- | -| URL | The Webhook URL. This field is [protected](ref:configure-contact-points#protected-fields) from modification in Grafana Cloud. | +| Option | Description | +| ------ | ------------------------------------------------------------------------------------------------------------ | +| URL | The Webhook URL. This field is [protected](ref:configure-contact-points) from modification in Grafana Cloud. | #### Optional settings diff --git a/docs/sources/alerting/set-up/configure-alert-state-history/index.md b/docs/sources/alerting/set-up/configure-alert-state-history/index.md index 6ab7f817d92..dfb476b69ef 100644 --- a/docs/sources/alerting/set-up/configure-alert-state-history/index.md +++ b/docs/sources/alerting/set-up/configure-alert-state-history/index.md @@ -62,6 +62,9 @@ The following steps describe a basic configuration: # The URL of the Loki server loki_remote_url = http://localhost:3100 + + [feature_toggles] + enable = alertingCentralAlertHistory ``` 1. **Configure the Loki data source in Grafana** diff --git a/docs/sources/alerting/set-up/configure-rbac/_index.md b/docs/sources/alerting/set-up/configure-rbac/_index.md index 33fb9e57dcb..6e3338bab0b 100644 --- a/docs/sources/alerting/set-up/configure-rbac/_index.md +++ b/docs/sources/alerting/set-up/configure-rbac/_index.md @@ -17,54 +17,166 @@ weight: 155 # Configure RBAC -Role-based access control (RBAC) for Grafana Enterprise and Grafana Cloud provides a standardized way of granting, changing, and revoking access, so that users can view and modify Grafana resources. +[Role-based access control (RBAC)](/docs/grafana/latest/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/) for Grafana Enterprise and Grafana Cloud provides a standardized way of granting, changing, and revoking access, so that users can view and modify Grafana resources. -A user is any individual who can log in to Grafana. Each user is associated with a role that includes permissions. Permissions determine the tasks a user can perform in the system. +A user is any individual who can log in to Grafana. Each user has a role that includes permissions. Permissions determine the tasks a user can perform in the system. Each permission contains one or more actions and a scope. +## Role types + +Grafana has three types of roles for managing access: + +- **Basic roles**: Admin, Editor, Viewer, and No basic role. These are assigned to users and provide default access levels. +- **Fixed roles**: Predefined groups of permissions for specific use cases. Basic roles automatically include certain fixed roles. +- **Custom roles**: User-defined roles that combine specific permissions for granular access control. + +## Basic role permissions + +The following table summarizes the default alerting permissions for each basic role. + +| Capability | Admin | Editor | Viewer | +| ----------------------------------------- | :---: | :----: | :----: | +| View alert rules | ✓ | ✓ | ✓ | +| Create, edit, and delete alert rules | ✓ | ✓ | | +| View silences | ✓ | ✓ | ✓ | +| Create, edit, and expire silences | ✓ | ✓ | | +| View contact points and templates | ✓ | ✓ | ✓ | +| Create, edit, and delete contact points | ✓ | ✓ | | +| View notification policies | ✓ | ✓ | ✓ | +| Create, edit, and delete policies | ✓ | ✓ | | +| View mute timings | ✓ | ✓ | ✓ | +| Create, edit, and delete timing intervals | ✓ | ✓ | | +| Access provisioning API | ✓ | ✓ | | +| Export with decrypted secrets | ✓ | | | + +{{< admonition type="note" >}} +Access to alert rules also requires permission to read the folder containing the rules and permission to query the data sources used in the rules. +{{< /admonition >}} + ## Permissions -Grafana Alerting has the following permissions. +Grafana Alerting has the following permissions organized by resource type. -| Action | Applicable scope | Description | -| -------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `alert.instances.external:read` | `datasources:*`
`datasources:uid:*` | Read alerts and silences in data sources that support alerting. | -| `alert.instances.external:write` | `datasources:*`
`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. | -| `alert.instances:create` | n/a | Create silences in the current organization. | -| `alert.instances:read` | n/a | Read alerts and silences in the current organization. | -| `alert.instances:write` | n/a | Update and expire silences in the current organization. | -| `alert.notifications.external:read` | `datasources:*`
`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. | -| `alert.notifications.external:write` | `datasources:*`
`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. | -| `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. | -| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. | -| `alert.rules.external:read` | `datasources:*`
`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) | -| `alert.rules.external:write` | `datasources:*`
`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). | -| `alert.rules:create` | `folders:*`
`folders:uid:*` | Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.rules:delete` | `folders:*`
`folders:uid:*` | Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. | -| `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. | -| `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. To allow query modifications add `datasources:query` in the scope of data sources the user can query. | -| `alert.silences:create` | `folders:*`
`folders:uid:*` | Create rule-specific silences in a folder and its subfolders. | -| `alert.silences:read` | `folders:*`
`folders:uid:*` | Read all general silences and rule-specific silences in a folder and its subfolders. | -| `alert.silences:write` | `folders:*`
`folders:uid:*` | Update and expire rule-specific silences in a folder and its subfolders. | -| `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and data source are not required. | -| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | -| `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and data source are not required. | -| `alert.provisioning.provenance:write` | n/a | Set provisioning status for alerting resources. Cannot be used alone. Requires user to have permissions to access resources | -| `alert.notifications.receivers:read` | `receivers:*`
`receivers:uid:*` | Read contact points. | -| `alert.notifications.receivers.secrets:read` | `receivers:*`
`receivers:uid:*` | Export contact points with decrypted secrets. | -| `alert.notifications.receivers:create` | n/a | Create a new contact points. The creator is automatically granted full access to the created contact point. | -| `alert.notifications.receivers:write` | `receivers:*`
`receivers:uid:*` | Update existing contact points. | -| `alert.notifications.receivers:delete` | `receivers:*`
`receivers:uid:*` | Update and delete existing contact points. | -| `receivers.permissions:read` | `receivers:*`
`receivers:uid:*` | Read permissions for contact points. | -| `receivers.permissions:write` | `receivers:*`
`receivers:uid:*` | Manage permissions for contact points. | -| `alert.notifications.time-intervals:read` | n/a | Read mute time intervals. | -| `alert.notifications.time-intervals:write` | n/a | Create new or update existing mute time intervals. | -| `alert.notifications.time-intervals:delete` | n/a | Delete existing time intervals. | -| `alert.notifications.templates:read` | n/a | Read templates. | -| `alert.notifications.templates:write` | n/a | Create new or update existing templates. | -| `alert.notifications.templates:delete` | n/a | Delete existing templates. | -| `alert.notifications.routes:read` | n/a | Read notification policies. | -| `alert.notifications.routes:write` | n/a | Create new, update and update notification policies. | +### Alert rules + +Permissions for managing Grafana-managed alert rules. + +| Action | Applicable scope | Description | +| -------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `alert.rules:create` | `folders:*`
`folders:uid:*` | Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. | +| `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. To allow query modifications add `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:delete` | `folders:*`
`folders:uid:*` | Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder. | + +### External alert rules + +Permissions for managing alert rules in external data sources that support alerting. + +| Action | Applicable scope | Description | +| ---------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `alert.rules.external:read` | `datasources:*`
`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki). | +| `alert.rules.external:write` | `datasources:*`
`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). | + +### Alert instances and silences + +Permissions for managing alert instances and silences in Grafana. + +| Action | Applicable scope | Description | +| ------------------------ | ------------------------------ | ------------------------------------------------------------------------------------ | +| `alert.instances:read` | n/a | Read alerts and silences in the current organization. | +| `alert.instances:create` | n/a | Create silences in the current organization. | +| `alert.instances:write` | n/a | Update and expire silences in the current organization. | +| `alert.silences:read` | `folders:*`
`folders:uid:*` | Read all general silences and rule-specific silences in a folder and its subfolders. | +| `alert.silences:create` | `folders:*`
`folders:uid:*` | Create rule-specific silences in a folder and its subfolders. | +| `alert.silences:write` | `folders:*`
`folders:uid:*` | Update and expire rule-specific silences in a folder and its subfolders. | + +### External alert instances + +Permissions for managing alert instances in external data sources. + +| Action | Applicable scope | Description | +| -------------------------------- | -------------------------------------- | ----------------------------------------------------------------- | +| `alert.instances.external:read` | `datasources:*`
`datasources:uid:*` | Read alerts and silences in data sources that support alerting. | +| `alert.instances.external:write` | `datasources:*`
`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. | + +### Contact points + +Permissions for managing contact points (notification receivers). + +| Action | Applicable scope | Description | +| -------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `alert.notifications.receivers:list` | n/a | List contact points in the current organization. | +| `alert.notifications.receivers:read` | `receivers:*`
`receivers:uid:*` | Read contact points. | +| `alert.notifications.receivers.secrets:read` | `receivers:*`
`receivers:uid:*` | Export contact points with decrypted secrets. | +| `alert.notifications.receivers:create` | n/a | Create a new contact points. The creator is automatically granted full access to the created contact point. | +| `alert.notifications.receivers:write` | `receivers:*`
`receivers:uid:*` | Update existing contact points. | +| `alert.notifications.receivers:delete` | `receivers:*`
`receivers:uid:*` | Update and delete existing contact points. | +| `alert.notifications.receivers:test` | `receivers:*`
`receivers:uid:*` | Test contact points to verify their configuration. | +| `receivers.permissions:read` | `receivers:*`
`receivers:uid:*` | Read permissions for contact points. | +| `receivers.permissions:write` | `receivers:*`
`receivers:uid:*` | Manage permissions for contact points. | + +### Notification policies + +Permissions for managing notification policies (routing rules). + +| Action | Applicable scope | Description | +| ---------------------------------- | ---------------- | ----------------------------------------------------- | +| `alert.notifications.routes:read` | n/a | Read notification policies. | +| `alert.notifications.routes:write` | n/a | Create new, update, and delete notification policies. | + +### Time intervals + +Permissions for managing mute time intervals. + +| Action | Applicable scope | Description | +| ------------------------------------------- | ---------------- | -------------------------------------------------- | +| `alert.notifications.time-intervals:read` | n/a | Read mute time intervals. | +| `alert.notifications.time-intervals:write` | n/a | Create new or update existing mute time intervals. | +| `alert.notifications.time-intervals:delete` | n/a | Delete existing time intervals. | + +### Templates + +Permissions for managing notification templates. + +| Action | Applicable scope | Description | +| ------------------------------------------ | ---------------- | ------------------------------------------------------------------------------- | +| `alert.notifications.templates:read` | n/a | Read templates. | +| `alert.notifications.templates:write` | n/a | Create new or update existing templates. | +| `alert.notifications.templates:delete` | n/a | Delete existing templates. | +| `alert.notifications.templates.test:write` | n/a | Test templates with custom payloads (preview and payload editor functionality). | + +### General notifications + +Legacy permissions for managing all notification resources. + +| Action | Applicable scope | Description | +| --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- | +| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. | +| `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. | + +### External notifications + +Permissions for managing notification resources in external data sources. + +| Action | Applicable scope | Description | +| ------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `alert.notifications.external:read` | `datasources:*`
`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. | +| `alert.notifications.external:write` | `datasources:*`
`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. | + +### Provisioning + +Permissions for managing alerting resources via the provisioning API. + +| Action | Applicable scope | Description | +| ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and data source are not required. | +| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | +| `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and data source are not required. | +| `alert.rules.provisioning:read` | n/a | Read Grafana alert rules via provisioning API. More specific than `alert.provisioning:read`. | +| `alert.rules.provisioning:write` | n/a | Create, update, and delete Grafana alert rules via provisioning API. More specific than `alert.provisioning:write`. | +| `alert.notifications.provisioning:read` | n/a | Read notification resources (contact points, notification policies, templates, time intervals) via provisioning API. More specific than `alert.provisioning:read`. | +| `alert.notifications.provisioning:write` | n/a | Create, update, and delete notification resources via provisioning API. More specific than `alert.provisioning:write`. | +| `alert.provisioning.provenance:write` | n/a | Set provisioning status for alerting resources. Cannot be used alone. Requires user to have permissions to access resources. | To help plan your RBAC rollout strategy, refer to [Plan your RBAC rollout strategy](https://grafana.com/docs/grafana/next/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/). diff --git a/docs/sources/alerting/set-up/configure-rbac/access-folders/index.md b/docs/sources/alerting/set-up/configure-rbac/access-folders/index.md index 10fb63385ff..825629089cb 100644 --- a/docs/sources/alerting/set-up/configure-rbac/access-folders/index.md +++ b/docs/sources/alerting/set-up/configure-rbac/access-folders/index.md @@ -16,7 +16,7 @@ title: Manage access using folders or data sources weight: 200 --- -## Manage access using folders or data sources +# Manage access using folders or data sources You can extend the access provided by a role to alert rules and rule-specific silences by assigning permissions to individual folders or data sources. diff --git a/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md b/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md index 668e84c41cb..b3c51d4f866 100644 --- a/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md +++ b/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md @@ -55,16 +55,16 @@ Details of the fixed roles and the access they provide for Grafana Alerting are | Full read-only access: `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read alert rules, alert instances, silences, contact points, and notification policies in Grafana and external providers. | | Read via Provisioning API + Export Secrets: `fixed:alerting.provisioning.secrets:reader` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read alert rules, alert instances, silences, contact points, and notification policies using the provisioning API and use export with decrypted secrets. | | Access to alert rules provisioning API: `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Manage all alert rules, notification policies, contact points, templates, in the organization using the provisioning API. | -| Set provisioning status: `fixed:alerting.provisioning.status:writer` | `alert.provisioning.provenance:write` | Set provisioning rules for Alerting resources. Should be used together with other regular roles (Notifications Writer and/or Rules Writer.) | +| Set provisioning status: `fixed:alerting.provisioning.provenance:writer` | `alert.provisioning.provenance:write` | Set provisioning rules for Alerting resources. Should be used together with other regular roles (Notifications Writer and/or Rules Writer.) | | Contact Point Reader: `fixed:alerting.receivers:reader` | `alert.notifications.receivers:read` for scope `receivers:*` | Read all contact points. | | Contact Point Creator: `fixed:alerting.receivers:creator` | `alert.notifications.receivers:create` | Create a new contact point. The user is automatically granted full access to the created contact point. | | Contact Point Writer: `fixed:alerting.receivers:writer` | `alert.notifications.receivers:read`, `alert.notifications.receivers:write`, `alert.notifications.receivers:delete` for scope `receivers:*` and
`alert.notifications.receivers:create` | Create a new contact point and manage all existing contact points. | | Templates Reader: `fixed:alerting.templates:reader` | `alert.notifications.templates:read` | Read all notification templates. | -| Templates Writer: `fixed:alerting.templates:writer` | `alert.notifications.templates:read`, `alert.notifications.templates:write`, `alert.notifications.templates:delete` | Create new and manage existing notification templates. | +| Templates Writer: `fixed:alerting.templates:writer` | `alert.notifications.templates:read`, `alert.notifications.templates:write`, `alert.notifications.templates:delete`, `alert.notifications.templates.test:write` | Create new and manage existing notification templates. Test templates with custom payloads. | | Time Intervals Reader: `fixed:alerting.time-intervals:reader` | `alert.notifications.time-intervals:read` | Read all time intervals. | | Time Intervals Writer: `fixed:alerting.time-intervals:writer` | `alert.notifications.time-intervals:read`, `alert.notifications.time-intervals:write`, `alert.notifications.time-intervals:delete` | Create new and manage existing time intervals. | -| Notification Policies Reader: `fixed:alerting.routes:reader` | `alert.notifications.routes:read` | Read all time intervals. | -| Notification Policies Writer: `fixed:alerting.routes:writer` | `alert.notifications.routes:read` `alert.notifications.routes:write` | Create new and manage existing time intervals. | +| Notification Policies Reader: `fixed:alerting.routes:reader` | `alert.notifications.routes:read` | Read all notification policies. | +| Notification Policies Writer: `fixed:alerting.routes:writer` | `alert.notifications.routes:read`
`alert.notifications.routes:write` | Create new and manage existing notification policies. | ## Create custom roles diff --git a/docs/sources/alerting/set-up/configure-roles/index.md b/docs/sources/alerting/set-up/configure-roles/index.md index 36adb865ab3..091d11de7bf 100644 --- a/docs/sources/alerting/set-up/configure-roles/index.md +++ b/docs/sources/alerting/set-up/configure-roles/index.md @@ -16,25 +16,27 @@ weight: 150 # Configure roles and permissions +This guide explains how to configure roles and permissions for Grafana Alerting for Grafana OSS users. You'll learn how to manage access using roles, folder permissions, and contact point permissions. + A user is any individual who can log in to Grafana. Each user is associated with a role that includes permissions. Permissions determine the tasks a user can perform in the system. For example, the Admin role includes permissions for an administrator to create and delete users. For more information, refer to [Organization roles](https://grafana.com/docs/grafana//administration/roles-and-permissions/#organization-roles). ## Manage access using roles -For Grafana OSS, there are three roles: Admin, Editor, and Viewer. +Grafana OSS has three roles: Admin, Editor, and Viewer. -Details of the roles and the access they provide for Grafana Alerting are below. +The following table describes the access each role provides for Grafana Alerting. -| Role | Access | -| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Admin | Write access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences), and provisioning. | -| Editor | Write access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences), and provisioning. | -| Viewer | Read access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences). | +| Role | Access | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Viewer | Read access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences). | +| Editor | Write access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences), and provisioning. | +| Admin | Write access to alert rules, notification resources (notification API, contact points, templates, time intervals, notification policies, and silences), and provisioning, as well as assign roles. | ## Assign roles -To assign roles, admins need to complete the following steps. +To assign roles, an admin needs to complete the following steps. 1. Navigate to **Administration** > **Users and access** > **Users, Teams, or Service Accounts**. 1. Search for the user, team or service account you want to add a role for. @@ -58,32 +60,30 @@ Refer to the following table for details on the additional access provided by fo You can't use folders to customize access to notification resources. {{< /admonition >}} -To manage folder permissions, complete the following steps. +To manage folder permissions, complete the following steps: 1. In the left-side menu, click **Dashboards**. 1. Hover your mouse cursor over a folder and click **Go to folder**. 1. Click **Manage permissions** from the Folder actions menu. 1. Update or add permissions as required. -## Manage access using contact point permissions +## Manage access to contact points -### Before you begin - -Extend or limit the access provided by a role to contact points by assigning permissions to individual contact point. +Extend or limit the access provided by a role to contact points by assigning permissions to individual contact points. This allows different users, teams, or service accounts to have customized access to read or modify specific contact points. Refer to the following table for details on the additional access provided by contact point permissions. -| Folder permission | Additional Access | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| View | View and export contact point as well as select it on the Alert rule edit page | -| Edit | Update or delete the contact point | -| Admin | Same additional access as Edit and manage permissions for the contact point. User should have additional permissions to read users and teams. | +| Contact point permission | Additional Access | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| View | View and export contact point as well as select it on the Alert rule edit page | +| Edit | Update or delete the contact point | +| Admin | Same additional access as Edit and manage permissions for the contact point. User should have additional permissions to read users and teams. | -### Steps +### Assign contact point permissions -To contact point permissions, complete the following steps. +To manage contact point permissions, complete the following steps: 1. In the left-side menu, click **Contact points**. 1. Hover your mouse cursor over a contact point and click **More**. diff --git a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md index 0531a0c803f..bfc369222c8 100644 --- a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md +++ b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md @@ -81,7 +81,7 @@ Replace the placeholders with your values: In your `grafana` directory, create a sub-folder called `dashboards`. -This guide shows you how to creates three separate dashboards. For all dashboard configurations, replace the placeholders with your values: +This guide shows you how to create three separate dashboards. For all dashboard configurations, replace the placeholders with your values: - _``_: Name of your Grafana Cloud Stack - _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster diff --git a/docs/sources/as-code/infrastructure-as-code/terraform/terraform-plugins.md b/docs/sources/as-code/infrastructure-as-code/terraform/terraform-plugins.md index f0626540ca3..eea698aace1 100644 --- a/docs/sources/as-code/infrastructure-as-code/terraform/terraform-plugins.md +++ b/docs/sources/as-code/infrastructure-as-code/terraform/terraform-plugins.md @@ -24,7 +24,7 @@ Before you begin, you should have the following available: - Administrator permissions in your Grafana instance; for more information on assigning Grafana RBAC roles, refer to [Assign RBAC roles](/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-control/assign-rbac-roles/). {{< admonition type="note" >}} -All of the following Terraform configuration files should be saved in the same directory. +Save all of the following Terraform configuration files in the same directory. {{< /admonition >}} ## Configure the Grafana provider diff --git a/docs/sources/as-code/observability-as-code/provision-resources/file-path-setup.md b/docs/sources/as-code/observability-as-code/provision-resources/file-path-setup.md index f2cd24a46b1..ca71a0c0d9c 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/file-path-setup.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/file-path-setup.md @@ -54,7 +54,7 @@ For production systems, use the `folderFromFilesStructure` capability instead of ## Before you begin {{< admonition type="note" >}} -Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. +Enable the `provisioning` feature toggle in Grafana to use this feature. {{< /admonition >}} To set up file provisioning, you need: @@ -67,7 +67,7 @@ To set up file provisioning, you need: ## Enable required feature toggles and configure permitted paths -To activate local file provisioning in Grafana, you need to enable the `provisioning` and `kubernetesDashboards` feature toggles. +To activate local file provisioning in Grafana, you need to enable the `provisioning` feature toggle. For additional information about feature toggles, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles). The local setting must be a relative path and its relative path must be configured in the `permitted_provisioned_paths` configuration option. @@ -82,12 +82,11 @@ Any subdirectories are automatically included. The values that you enter for the `permitted_provisioning_paths` become the base paths for those entered when you enter a local path in the **Connect to local storage** wizard. 1. Open your Grafana configuration file, either `grafana.ini` or `custom.ini`. For file location based on operating system, refer to [Configuration file location](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#experimental-feature-toggles). -1. Locate or add a `[feature_toggles]` section. Add these values: +1. Locate or add a `[feature_toggles]` section. Add this value: ```ini [feature_toggles] provisioning = true - kubernetesDashboards = true ; use k8s from browser ``` 1. Locate or add a `[paths]` section. To add more than one location, use the pipe character (`|`) to separate the paths. The list should not include empty paths or trailing pipes. Add these values: @@ -129,35 +128,48 @@ The set up process verifies the path and provides an error message if a problem #### Synchronization limitations -Full instance sync is not available in Grafana Cloud. +{{< admonition type="caution" >}} -In Grafana OSS/Enterprise: +Full instance sync is not available in Grafana Cloud and is experimental and unsupported in Grafana OSS/Enterprise. + +{{< /admonition >}} + +To have access to full instance sync you must explicitly enable the option. + +The following applies: -- 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). +You can sync external resources into a new folder without affecting the rest of your instance. -- 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. +To set up synchronization: -- 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. Select which resources you want to sync. -Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. +1. 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. +1. Click **Synchronize** to continue. + +1. You can repeat this process for up to 10 connections. + +{{< admonition type="note" >}} + +Optionally, you can export any unmanaged resources into the provisioned folder. See how in [Synchronize with external storage](#synchronize-with-external-storage). + +{{< /admonition >}} ### Synchronize with external storage -After this one time step, all future updates are automatically saved to the local file path and provisioned back to the instance. +In this step you proceed to synchronize the resources selected in the previous step. Optionally, you can check the **Migrate existing resources** box to migrate your unmanaged dashboards to the provisioned folder. -During the initial synchronization, your dashboards will be temporarily unavailable. No data or configurations will be lost. +Select **Begin synchronization** to start the process. After this one time step, all future updates are automatically saved to the local file path and provisioned back to the instance. + +Note that during the initial synchronization, your dashboards will be temporarily unavailable. No data or configurations will be lost. How long the process takes depends upon the number of resources involved. -Select **Begin synchronization** to start the process. - ### Choose additional settings If you wish, you can make any files synchronized as as **Read only** so no changes can be made to the resources through Grafana. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/_index.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/_index.md new file mode 100644 index 00000000000..4e4e523bf35 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/_index.md @@ -0,0 +1,147 @@ +--- +title: Git Sync deployment scenarios +menuTitle: Deployment scenarios +description: Learn about common Git Sync deployment patterns and configurations for different organizational needs +weight: 450 +keywords: + - git sync + - deployment patterns + - scenarios + - multi-environment + - teams +--- + +# Git Sync deployment scenarios + +This guide shows practical deployment scenarios for Grafana’s Git Sync. Learn how to configure bidirectional synchronization between Grafana and Git repositories for teams, environments, and regions. + +{{< admonition type="caution" >}} +Git Sync is an experimental feature. It reflects Grafana’s approach to Observability as Code and might include limitations or breaking changes. For current status and known limitations, refer to the [Git Sync introduction](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/intro-git-sync/). +{{< /admonition >}} + +## Understand the relationship between key Git Sync components + +Before you explore the scenarios, understand how the key Git Sync components relate: + +- [Grafana instance](#grafana-instance) +- [Git repository structure](#git-repository-structure) +- [Git Sync repository resource](#git-sync-repository-resource) + +### Grafana instance + +A Grafana instance is a running Grafana server. Multiple instances can: + +- Connect to the same Git repository using different Repository configurations. +- Sync from different branches of the same repository. +- Sync from different paths within the same repository. +- Sync from different repositories. + +### Git repository structure + +You can organize your Git repository in several ways: + +- Single branch, multiple paths: Use different directories for different purposes (for example, `dev/`, `prod/`, `team-a/`). +- Multiple branches: Use different branches for different environments or teams (for example, `main`, `develop`, `team-a`). +- Multiple repositories: Use separate repositories for different teams or environments. + +### Git Sync repository resource + +A repository resource is a Grafana configuration object that defines: + +- Which Git repository to sync with. +- Which branch to use. +- Which directory path to synchronize. +- Sync behavior and workflows. + +Each repository resource creates bidirectional synchronization between a Grafana instance and a specific location in Git. + +## How does repository sync behave? + +With Git Sync you configure a repository resource to sync with your Grafana instance: + +1. Grafana monitors the specified Git location (repository, branch, and path). +2. Grafana creates a folder in Dashboards (typically named after the repository). +3. Grafana creates dashboards from dashboard JSON files in Git within this folder. +4. Grafana commits dashboard changes made in the UI back to Git. +5. Grafana pulls dashboard changes made in Git and updates dashboards in the UI. +6. Synchronization occurs at regular intervals (configurable), or instantly if you use webhooks. + +You can find the provisioned dashboards organized in folders under **Dashboards**. + +## Example: Relationship between repository, branch, and path + +Here's a concrete example showing how the three parameters work together: + +**Configuration:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `team-platform/grafana/` + +**In Git (on branch `main`):** + +``` +your-org/grafana-manifests/ +├── .git/ +├── README.md +├── team-platform/ +│ └── grafana/ +│ ├── cpu-metrics.json ← Synced +│ ├── memory-usage.json ← Synced +│ └── disk-io.json ← Synced +├── team-data/ +│ └── grafana/ +│ └── pipeline-stats.json ← Not synced (different path) +└── other-files.txt ← Not synced (outside path) +``` + +**In Grafana Dashboards view:** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── CPU Metrics Dashboard + ├── Memory Usage Dashboard + └── Disk I/O Dashboard +``` + +**Key points:** + +- Grafana only synchronizes files within the specified path (`team-platform/grafana/`). +- Grafana ignores files in other paths or at the repository root. +- The folder name in Grafana comes from the repository name. +- Dashboard titles come from the JSON file content, not the filename. + +## Repository configuration flexibility + +Git Sync repositories support different combinations of repository URL, branch, and path: + +- Different Git repositories: Each environment or team can use its own repository. + - Instance A: `repository: your-org/grafana-prod`. + - Instance B: `repository: your-org/grafana-dev`. +- Different branches: Use separate branches within the same repository. + - Instance A: `repository: your-org/grafana-manifests, branch: main`. + - Instance B: `repository: your-org/grafana-manifests, branch: develop`. +- Different paths: Use different directory paths within the same repository. + - Instance A: `repository: your-org/grafana-manifests, branch: main, path: production/`. + - Instance B: `repository: your-org/grafana-manifests, branch: main, path: development/`. +- Any combination: Mix and match based on your workflow requirements. + +## Scenarios + +Use these deployment scenarios to plan your Git Sync setup: + +- [Single instance](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/single-instance/) +- [Git Sync for development and production environments](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/dev-prod/) +- [Git Sync with regional replication](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-region/) +- [High availability](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/high-availability/) +- [Git Sync in a shared Grafana instance](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-team/) + +## Learn more + +Refer to the following documents to learn more: + +- [Git Sync introduction](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/intro-git-sync/) +- [Git Sync setup guide](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-setup/) +- [Dashboard provisioning](https://grafana.com/docs/grafana//administration/provisioning/) +- [Observability as Code](https://grafana.com/docs/grafana//as-code/observability-as-code/) diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/dev-prod.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/dev-prod.md new file mode 100644 index 00000000000..3433c713024 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/dev-prod.md @@ -0,0 +1,147 @@ +--- +title: Git Sync for development and production environments +menuTitle: Across environments +description: Use separate Grafana instances for development and production with Git-controlled promotion +weight: 20 +--- + +# Git Sync for development and production environments + +Use separate Grafana instances for development and production. Each syncs with different Git locations to test dashboards before production. + +## Use it for + +- **Staged deployments**: You need to test dashboard changes before production deployment. +- **Change control**: You require approvals before dashboards reach production. +- **Quality assurance**: You verify dashboard functionality in a non-production environment. +- **Risk mitigation**: You minimize the risk of breaking production dashboards. + +## Architecture + +``` +┌────────────────────────────────────────────────────────────┐ +│ GitHub Repository │ +│ Repository: your-org/grafana-manifests │ +│ Branch: main │ +│ │ +│ grafana-manifests/ │ +│ ├── dev/ │ +│ │ ├── dashboard-new.json ← Development dashboards │ +│ │ └── dashboard-test.json │ +│ │ │ +│ └── prod/ │ +│ ├── dashboard-stable.json ← Production dashboards │ +│ └── dashboard-approved.json │ +└────────────────────────────────────────────────────────────┘ + ↕ ↕ + Git Sync (dev/) Git Sync (prod/) + ↕ ↕ +┌─────────────────────┐ ┌─────────────────────┐ +│ Dev Grafana │ │ Prod Grafana │ +│ │ │ │ +│ Repository: │ │ Repository: │ +│ - path: dev/ │ │ - path: prod/ │ +│ │ │ │ +│ Creates folder: │ │ Creates folder: │ +│ "grafana-manifests"│ │ "grafana-manifests"│ +└─────────────────────┘ └─────────────────────┘ +``` + +## Repository structure + +**In Git:** + +``` +your-org/grafana-manifests +├── dev/ +│ ├── dashboard-new.json +│ └── dashboard-test.json +└── prod/ + ├── dashboard-stable.json + └── dashboard-approved.json +``` + +**In Grafana Dashboards view:** + +**Dev instance:** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── New Dashboard + └── Test Dashboard +``` + +**Prod instance:** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── Stable Dashboard + └── Approved Dashboard +``` + +- Both instances create a folder named "grafana-manifests" (from repository name) +- Each instance only shows dashboards from its configured path (`dev/` or `prod/`) +- Dashboards appear with their titles from the JSON files + +## Configuration parameters + +Development: + +- Repository: `your-org/grafana-manifests` +- Branch: `main` +- Path: `dev/` + +Production: + +- Repository: `your-org/grafana-manifests` +- Branch: `main` +- Path: `prod/` + +## How it works + +1. Developers create and modify dashboards in development. +2. Git Sync commits changes to `dev/`. +3. You review changes in Git. +4. You promote approved dashboards from `dev/` to `prod/`. +5. Production syncs from `prod/`. +6. Production dashboards update. + +## Alternative: Use branches + +Instead of using different paths, you can configure instances to use different branches: + +**Development instance:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `develop` +- **Path**: `grafana/` + +**Production instance:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `grafana/` + +With this approach: + +- Development changes go to the `develop` branch +- Use Git merge or pull request workflows to promote changes from `develop` to `main` +- Production automatically syncs from the `main` branch + +## Alternative: Use separate repositories for stricter isolation + +For stricter isolation, use completely separate repositories: + +**Development instance:** + +- **Repository**: `your-org/grafana-manifests-dev` +- **Branch**: `main` +- **Path**: `grafana/` + +**Production instance:** + +- **Repository**: `your-org/grafana-manifests-prod` +- **Branch**: `main` +- **Path**: `grafana/` diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/high-availability.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/high-availability.md new file mode 100644 index 00000000000..04575574ed4 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/high-availability.md @@ -0,0 +1,217 @@ +--- +title: Git Sync for high availability environments +menuTitle: High availability +description: Run multiple Grafana instances serving traffic simultaneously, synchronized via Git Sync +weight: 50 +--- + +# Git Sync for high availability environments + +## Primary–replica scenario + +Use a primary Grafana instance and one or more replicas synchronized with the same Git location to enable failover. + +### Use it for + +- **Automatic failover**: You need service continuity when the primary instance fails. +- **High availability**: Your organization requires guaranteed dashboard availability. +- **Simple HA setup**: You want high availability without the complexity of active–active. +- **Maintenance windows**: You perform updates while another instance serves traffic. +- **Business continuity**: Dashboard access can't tolerate downtime. + +### Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GitHub Repository │ +│ Repository: your-org/grafana-manifests │ +│ Branch: main │ +│ │ +│ grafana-manifests/ │ +│ └── shared/ │ +│ ├── dashboard-metrics.json │ +│ ├── dashboard-alerts.json │ +│ └── dashboard-logs.json │ +└─────────────────────────────────────────────────────┘ + ↕ ↕ + Git Sync (shared/) Git Sync (shared/) + ↕ ↕ +┌────────────────────┐ ┌────────────────────┐ +│ Master Grafana │ │ Replica Grafana │ +│ (Active) │ │ (Standby) │ +│ │ │ │ +│ Repository: │ │ Repository: │ +│ - path: shared/ │ │ - path: shared/ │ +└────────────────────┘ └────────────────────┘ + │ │ + └───────────┬───────────────────┘ + ↓ + ┌──────────────────────┐ + │ Reverse Proxy │ + │ (Failover) │ + └──────────────────────┘ +``` + +### Repository structure + +**In Git:** + +``` +your-org/grafana-manifests +└── shared/ + ├── dashboard-metrics.json + ├── dashboard-alerts.json + └── dashboard-logs.json +``` + +**In Grafana Dashboards view (both instances):** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── Metrics Dashboard + ├── Alerts Dashboard + └── Logs Dashboard +``` + +- Master and replica instances show identical folder structure. +- Both sync from the same `shared/` path. +- Reverse proxy routes traffic to master (active) instance. +- If master fails, proxy automatically fails over to replica (standby). +- Users see the same dashboards regardless of which instance is serving traffic. + +### Configuration parameters + +Both master and replica instances use identical parameters: + +**Master instance:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `shared/` + +**Replica instance:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `shared/` + +### How it works + +1. Both instances stay synchronized through Git. +2. Reverse proxy routes traffic to primary. +3. Users edit on primary. Git Sync commits changes. +4. Both instances pull latest changes to keep replica in sync. +5. On primary failure, proxy fails over to replica. + +### Failover considerations + +- Health checks and monitoring. +- Continuous syncing to minimize data loss. +- Plan failback (automatic or manual). + +## Load balancer scenario + +Run multiple active Grafana instances behind a load balancer. All instances sync from the same Git location. + +### Use it for + +- **High traffic**: Your deployment needs to handle significant user load. +- **Load distribution**: You want to distribute user requests across instances. +- **Maximum availability**: You need service continuity during maintenance or failures. +- **Scalability**: You want to add instances as load increases. +- **Performance**: Users need fast response times under heavy load. + +### Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GitHub Repository │ +│ Repository: your-org/grafana-manifests │ +│ Branch: main │ +│ │ +│ grafana-manifests/ │ +│ └── shared/ │ +│ ├── dashboard-metrics.json │ +│ ├── dashboard-alerts.json │ +│ └── dashboard-logs.json │ +└─────────────────────────────────────────────────────┘ + ↕ ↕ + Git Sync (shared/) Git Sync (shared/) + ↕ ↕ +┌────────────────────┐ ┌────────────────────┐ +│ Grafana Instance 1│ │ Grafana Instance 2│ +│ (Active) │ │ (Active) │ +│ │ │ │ +│ Repository: │ │ Repository: │ +│ - path: shared/ │ │ - path: shared/ │ +└────────────────────┘ └────────────────────┘ + │ │ + └───────────┬───────────────────┘ + ↓ + ┌──────────────────────┐ + │ Load Balancer │ + │ (Round Robin) │ + └──────────────────────┘ +``` + +### Repository structure + +**In Git:** + +``` +your-org/grafana-manifests +└── shared/ + ├── dashboard-metrics.json + ├── dashboard-alerts.json + └── dashboard-logs.json +``` + +**In Grafana Dashboards view (all instances):** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── Metrics Dashboard + ├── Alerts Dashboard + └── Logs Dashboard +``` + +- All instances show identical folder structure. +- All instances sync from the same `shared/` path. +- Load balancer distributes requests across all active instances. +- Any instance can serve read requests. +- Any instance can accept dashboard modifications. +- Changes propagate to all instances through Git. + +### Configuration parameters + +All instances use identical parameters: + +**Instance 1:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `shared/` + +**Instance 2:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `shared/` + +### How it works + +1. All instances stay synchronized through Git. +2. Load balancer distributes incoming traffic across all active instances. +3. Users can view dashboards from any instance. +4. When a user modifies a dashboard on any instance, Git Sync commits the change. +5. All other instances pull the updated dashboard during their next sync cycle, or instantly if webhooks are configured. +6. If one instance fails, load balancer stops routing traffic to it and remaining instances continue serving. + +### Important considerations + +- **Eventually consistent**: Due to sync intervals, instances may briefly have different dashboard versions. +- **Concurrent edits**: Multiple users editing the same dashboard on different instances can cause conflicts. +- **Database sharing**: Instances should share the same backend database for user sessions, preferences, and annotations. +- **Stateless design**: Design for stateless operation where possible to maximize load balancing effectiveness. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-region.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-region.md new file mode 100644 index 00000000000..ee699719560 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-region.md @@ -0,0 +1,93 @@ +--- +title: Git Sync with regional replication +menuTitle: Regional replication +description: Synchronize multiple regional Grafana instances from a shared Git location +weight: 30 +--- + +# Git Sync with regional replication + +Deploy multiple Grafana instances across regions. Synchronize them with the same Git location to ensure consistent dashboards everywhere. + +## Use it for + +- **Geographic distribution**: You deploy Grafana close to users in different regions. +- **Latency reduction**: Users need fast dashboard access from their location. +- **Data sovereignty**: You keep dashboard data in specific regions. +- **High availability**: You need dashboard availability across regions. +- **Consistent experience**: All users see the same dashboards regardless of region. + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GitHub Repository │ +│ Repository: your-org/grafana-manifests │ +│ Branch: main │ +│ │ +│ grafana-manifests/ │ +│ └── shared/ │ +│ ├── dashboard-global.json │ +│ ├── dashboard-metrics.json │ +│ └── dashboard-logs.json │ +└─────────────────────────────────────────────────────┘ + ↕ ↕ + Git Sync (shared/) Git Sync (shared/) + ↕ ↕ +┌────────────────────┐ ┌────────────────────┐ +│ US Region │ │ EU Region │ +│ Grafana │ │ Grafana │ +│ │ │ │ +│ Repository: │ │ Repository: │ +│ - path: shared/ │ │ - path: shared/ │ +└────────────────────┘ └────────────────────┘ +``` + +## Repository structure + +**In Git:** + +``` +your-org/grafana-manifests +└── shared/ + ├── dashboard-global.json + ├── dashboard-metrics.json + └── dashboard-logs.json +``` + +**In Grafana Dashboards view (all regions):** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── Global Dashboard + ├── Metrics Dashboard + └── Logs Dashboard +``` + +- All regional instances (US, EU, etc.) show identical folder structure +- Same folder name "grafana-manifests" in every region +- Same dashboards synced from the `shared/` path appear everywhere +- Users in any region see the exact same dashboards with the same titles + +## Configuration parameters + +All regions: + +- Repository: `your-org/grafana-manifests` +- Branch: `main` +- Path: `shared/` + +## How it works + +1. All regional instances pull dashboards from `shared/`. +2. Any region’s change commits to Git. +3. Other regions pull updates during the next sync (or via webhooks). +4. Changes propagate across regions per sync interval. + +## Considerations + +- **Write conflicts**: If users in different regions modify the same dashboard simultaneously, Git uses last-write-wins. +- **Primary region**: Consider designating one region as the primary location for making dashboard changes. +- **Propagation time**: Changes propagate to all regions within the configured sync interval, or instantly if webhooks are configured. +- **Network reliability**: Ensure all regions have reliable connectivity to the Git repository. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-team.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-team.md new file mode 100644 index 00000000000..4f5ecc85fd2 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/multi-team.md @@ -0,0 +1,169 @@ +--- +title: Multiple team Git Sync +menuTitle: Shared instance +description: Use multiple Git repositories with one Grafana instance, one repository per team +weight: 60 +--- + +# Git Sync in a Grafana instance shared by multiple teams + +Use a single Grafana instance with multiple Repository resources, one per team. Each team manages its own dashboards while sharing Grafana. + +## Use it for + +- **Team autonomy**: Different teams manage their own dashboards independently. +- **Organizational structure**: Dashboard organization aligns with team structure. +- **Resource efficiency**: Multiple teams share Grafana infrastructure. +- **Cost optimization**: You reduce infrastructure costs while maintaining team separation. +- **Collaboration**: Teams can view each other’s dashboards while managing their own. + +## Architecture + +``` +┌─────────────────────────┐ ┌─────────────────────────┐ +│ Platform Team Repo │ │ Data Team Repo │ +│ platform-dashboards │ │ data-dashboards │ +│ │ │ │ +│ platform-dashboards/ │ │ data-dashboards/ │ +│ └── grafana/ │ │ └── grafana/ │ +│ ├── k8s.json │ │ ├── pipeline.json │ +│ └── infra.json │ │ └── analytics.json │ +└─────────────────────────┘ └─────────────────────────┘ + ↕ ↕ + Git Sync (grafana/) Git Sync (grafana/) + ↕ ↕ + ┌──────────────────────────────────────┐ + │ Grafana Instance │ + │ │ + │ Repository 1: │ + │ - repo: platform-dashboards │ + │ → Creates "platform-dashboards" │ + │ │ + │ Repository 2: │ + │ - repo: data-dashboards │ + │ → Creates "data-dashboards" │ + └──────────────────────────────────────┘ +``` + +## Repository structure + +**In Git (separate repositories):** + +**Platform team repository:** + +``` +your-org/platform-dashboards +└── grafana/ + ├── dashboard-k8s.json + └── dashboard-infra.json +``` + +**Data team repository:** + +``` +your-org/data-dashboards +└── grafana/ + ├── dashboard-pipeline.json + └── dashboard-analytics.json +``` + +**In Grafana Dashboards view:** + +``` +Dashboards +├── 📁 platform-dashboards/ +│ ├── Kubernetes Dashboard +│ └── Infrastructure Dashboard +└── 📁 data-dashboards/ + ├── Pipeline Dashboard + └── Analytics Dashboard +``` + +- Two separate folders created (one per Repository resource). +- Folder names derived from repository names. +- Each team has complete control over their own repository. +- Teams can independently manage permissions, branches, and workflows in their repos. +- All teams can view each other's dashboards in Grafana but manage only their own. + +## Configuration parameters + +**Platform team repository:** + +- **Repository**: `your-org/platform-dashboards` +- **Branch**: `main` +- **Path**: `grafana/` + +**Data team repository:** + +- **Repository**: `your-org/data-dashboards` +- **Branch**: `main` +- **Path**: `grafana/` + +## How it works + +1. Each team has their own Git repository for complete autonomy. +2. Each repository resource in Grafana creates a separate folder. +3. Platform team dashboards sync from `your-org/platform-dashboards` repository. +4. Data team dashboards sync from `your-org/data-dashboards` repository. +5. Teams can independently manage their repository settings, access controls, and workflows. +6. All teams can view each other's dashboards in Grafana but edit only their own. + +## Scale to more teams + +Adding additional teams is straightforward. For a third team, create a new repository and configure: + +- **Repository**: `your-org/security-dashboards` +- **Branch**: `main` +- **Path**: `grafana/` + +This creates a new "security-dashboards" folder in the same Grafana instance. + +## Alternative: Shared repository with different paths + +For teams that prefer sharing a single repository, use different paths to separate team dashboards: + +**In Git:** + +``` +your-org/grafana-manifests +├── team-platform/ +│ ├── dashboard-k8s.json +│ └── dashboard-infra.json +└── team-data/ + ├── dashboard-pipeline.json + └── dashboard-analytics.json +``` + +**Configuration:** + +**Platform team:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `team-platform/` + +**Data team:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `team-data/` + +This approach provides simpler repository management but less isolation between teams. + +## Alternative: Different branches per team + +For teams wanting their own branch in a shared repository: + +**Platform team:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `team-platform` +- **Path**: `grafana/` + +**Data team:** + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `team-data` +- **Path**: `grafana/` + +This allows teams to use Git branch workflows for collaboration while sharing the same repository. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/single-instance.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/single-instance.md new file mode 100644 index 00000000000..39371f7e7d6 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios/single-instance.md @@ -0,0 +1,86 @@ +--- +title: Single instance Git Sync +menuTitle: Single instance +description: Synchronize a single Grafana instance with a Git repository +weight: 10 +--- + +# Single instance Git Sync + +Use a single Grafana instance synchronized with a Git repository. This is the foundation for Git Sync and helps you understand bidirectional synchronization. + +## Use it for + +- **Getting started**: You want to learn how Git Sync works before implementing complex scenarios. +- **Personal projects**: Individual developers manage their own dashboards. +- **Small teams**: You have a simple setup without multiple environments or complex workflows. +- **Development environments**: You need quick prototyping and testing. + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GitHub Repository │ +│ Repository: your-org/grafana-manifests │ +│ Branch: main │ +│ │ +│ grafana-manifests/ │ +│ └── grafana/ │ +│ ├── dashboard-1.json │ +│ ├── dashboard-2.json │ +│ └── dashboard-3.json │ +└─────────────────────────────────────────────────────┘ + ↕ + Git Sync (bidirectional) + ↕ + ┌─────────────────────────────┐ + │ Grafana Instance │ + │ │ + │ Repository Resource: │ + │ - url: grafana-manifests │ + │ - branch: main │ + │ - path: grafana/ │ + │ │ + │ Creates folder: │ + │ "grafana-manifests" │ + └─────────────────────────────┘ +``` + +## Repository structure + +**In Git:** + +``` +your-org/grafana-manifests +└── grafana/ + ├── dashboard-1.json + ├── dashboard-2.json + └── dashboard-3.json +``` + +**In Grafana Dashboards view:** + +``` +Dashboards +└── 📁 grafana-manifests/ + ├── Dashboard 1 + ├── Dashboard 2 + └── Dashboard 3 +``` + +- A folder named "grafana-manifests" (from repository name) contains all synced dashboards. +- Each JSON file becomes a dashboard with its title displayed in the folder. +- Users browse dashboards organized under this folder structure. + +## Configuration parameters + +Configure your Grafana instance to synchronize with: + +- **Repository**: `your-org/grafana-manifests` +- **Branch**: `main` +- **Path**: `grafana/` + +## How it works + +1. **From Grafana to Git**: When users create or modify dashboards in Grafana, Git Sync commits changes to the `grafana/` directory on the `main` branch. +2. **From Git to Grafana**: When dashboard JSON files are added or modified in the `grafana/` directory, Git Sync pulls these changes into Grafana. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md index f6113fe428d..b43c279fb44 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md @@ -29,76 +29,70 @@ You can sign up to the private preview using the [Git Sync early access form](ht {{< /admonition >}} -Git Sync lets you manage Grafana dashboards as code by storing dashboard JSON files and folders in a remote GitHub repository. - -To set up Git Sync and synchronize with a GitHub repository follow these steps: - -1. [Enable feature toggles in Grafana](#enable-required-feature-toggles) (first time set up). -1. [Create a GitHub access token](#create-a-github-access-token). -1. [Configure a connection to your GitHub repository](#set-up-the-connection-to-github). -1. [Choose what content to sync with Grafana](#choose-what-to-synchronize). - -Optionally, you can [extend Git Sync](#configure-webhooks-and-image-rendering) by enabling pull request notifications and image previews of dashboard changes. - -| Capability | Benefit | Requires | -| ----------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------- | -| Adds a table summarizing changes to your pull request | Provides a convenient way to save changes back to GitHub. | Webhooks configured | -| Add a dashboard preview image to a PR | View a snapshot of dashboard changes to a pull request without opening Grafana. | Image renderer and webhooks configured | - -{{< admonition type="note" >}} - -Alternatively, you can configure a local file system instead of using GitHub. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/) for more information. - -{{< /admonition >}} - -## Performance impacts of enabling Git Sync - -Git Sync is an experimental feature and is under continuous development. Reporting any issues you encounter can help us improve Git Sync. - -When Git Sync is enabled, the database load might increase, especially for instances with a lot of folders and nested folders. Evaluate the performance impact, if any, in a non-production environment. +This guide shows you how to set up Git Sync to synchronize your Grafana dashboards and folders with a GitHub repository. You'll set up Git Sync to enable version-controlled dashboard management either [using the UI](#set-up-git-sync-using-grafana-ui) or [as code](#set-up-git-sync-as-code). ## Before you begin -{{< admonition type="caution" >}} +Before you begin, ensure you have the following: -Refer to [Known limitations](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync#known-limitations/) before using Git Sync. +- A Grafana instance (Cloud, OSS, or Enterprise). +- If you're [using webhooks or image rendering](#extend-git-sync-for-real-time-notification-and-image-rendering), a public instance with external access +- Administration rights in your Grafana organization +- A [GitHub private access token](#create-a-github-access-token) +- A GitHub repository to store your dashboards in +- Optional: The [Image Renderer service](https://github.com/grafana/grafana-image-renderer) to save image previews with your PRs + +### Known limitations + +Refer to [Known limitations](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/intro-git-sync#known-limitations) before using Git Sync. + +Refer to [Supported resources](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/intro-git-sync#supported-resources) for details about which resources you can sync. + +### Performance considerations + +When Git Sync is enabled, the database load might increase, especially for instances with many folders and nested folders. Evaluate the performance impact, if any, in a non-production environment. + +Git Sync is under continuous development. [Report any issues](https://grafana.com/help/) you encounter to help us improve Git Sync. + +## Set up Git Sync + +To set up Git Sync and synchronize with a GitHub repository, follow these steps: + +1. [Enable feature toggles in Grafana](#enable-required-feature-toggles) (first time setup) +1. [Create a GitHub access token](#create-a-github-access-token) +1. Set up Git Sync [using the UI](#set-up-git-sync-using-grafana-ui) or [as code](#set-up-git-sync-as-code) + +After setup, you can [verify your dashboards](#verify-your-dashboards-in-grafana). + +Optionally, you can also [extend Git Sync with webhooks and image rendering](#extend-git-sync-for-real-time-notification-and-image-rendering). + +{{< admonition type="note" >}} + +Alternatively, you can configure a local file system instead of using GitHub. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/file-path-setup/) for more information. {{< /admonition >}} -### Requirements - -To set up Git Sync, you need: - -- Administration rights in your Grafana organization. -- Enable the required feature toggles in your Grafana instance. Refer to [Enable required feature toggles](#enable-required-feature-toggles) for instructions. -- A GitHub repository to store your dashboards in. - - If you want to use a local file path, refer to [the local file path guide](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/). -- A GitHub access token. The Grafana UI will prompt you during setup. -- Optional: A public Grafana instance. -- Optional: The [Image Renderer service](https://github.com/grafana/grafana-image-renderer) to save image previews with your PRs. - ## Enable required feature toggles -To activate Git Sync in Grafana, you need to enable the `provisioning` and `kubernetesDashboards` feature toggles. -For additional information about feature toggles, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles). +To activate Git Sync in Grafana, you need to enable the `provisioning` feature toggle. For more information about feature toggles, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#experimental-feature-toggles). -To enable the required feature toggles, add them to your Grafana configuration file: +To enable the required feature toggle: 1. Open your Grafana configuration file, either `grafana.ini` or `custom.ini`. For file location based on operating system, refer to [Configuration file location](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#experimental-feature-toggles). -1. Locate or add a `[feature_toggles]` section. Add these values: +1. Locate or add a `[feature_toggles]` section. Add this value: ```ini [feature_toggles] provisioning = true - kubernetesDashboards = true ; use k8s from browser ``` 1. Save the changes to the file and restart Grafana. ## Create a GitHub access token -Whenever you connect to a GitHub repository, you need to create a GitHub access token with specific repository permissions. -This token needs to be added to your Git Sync configuration to enable read and write permissions between Grafana and GitHub repository. +Whenever you connect to a GitHub repository, you need to create a GitHub access token with specific repository permissions. This token needs to be added to your Git Sync configuration to enable read and write permissions between Grafana and GitHub repository. + +To create a GitHub access token: 1. Create a new token using [Create new fine-grained personal access token](https://github.com/settings/personal-access-tokens/new). Refer to [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for instructions. 1. Under **Permissions**, expand **Repository permissions**. @@ -112,19 +106,23 @@ This token needs to be added to your Git Sync configuration to enable read and w 1. Verify the options and select **Generate token**. 1. Copy the access token. Leave the browser window available with the token until you've completed configuration. -GitHub Apps are not currently supported. +GitHub Apps aren't currently supported. -## Set up the connection to GitHub +## Set up Git Sync using Grafana UI -Use **Provisioning** to guide you through setting up Git Sync to use a GitHub repository. +1. [Configure a connection to your GitHub repository](#set-up-the-connection-to-github) +1. [Choose what content to sync with Grafana](#choose-what-to-synchronize) +1. [Choose additional settings](#choose-additional-settings) + +### Set up the connection to GitHub + +Use **Provisioning** to guide you through setting up Git Sync to use a GitHub repository: 1. Log in to your Grafana server with an account that has the Grafana Admin flag set. 1. Select **Administration** in the left-side menu and then **Provisioning**. 1. Select **Configure Git Sync**. -### Connect to external storage - -To connect your GitHub repository, follow these steps: +To connect your GitHub repository: 1. Paste your GitHub personal access token into **Enter your access token**. Refer to [Create a GitHub access token](#create-a-github-access-token) for instructions. 1. Paste the **Repository URL** for your GitHub repository into the text box. @@ -134,56 +132,217 @@ 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. +You can sync external resources into a new folder without affecting the rest of your 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. +To set up synchronization: -#### Synchronization limitations +1. Select which resources you want to sync. -Git Sync only supports dashboards and folders. Alerts, panels, and other resources are not supported yet. +1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. -{{< admonition type="caution" >}} +1. Click **Synchronize** to continue. -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. +1. You can repeat this process for up to 10 connections. + +{{< admonition type="note" >}} + +Optionally, you can export any unmanaged resources into the provisioned folder. See how in [Synchronize with external storage](#synchronize-with-external-storage). {{< /admonition >}} -Full instance sync is not available in Grafana Cloud. +#### Full instance sync -In Grafana OSS/Enterprise: +Full instance sync is not available in Grafana Cloud and is experimental and unsupported 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. +To have access to this option you must enable experimental instance sync on purpose. -#### Set up synchronization +### Synchronize with external storage -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). +After this one time step, all future updates are automatically saved to the Git repository and provisioned back to the instance. -- 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. - -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. +Check the **Migrate existing resources** box to migrate your unmanaged dashboards to the provisioned folder. ### Choose additional settings Finally, you can set up how often your configured storage is polled for updates. +To configure additional settings: + 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. Optional: If you have the Grafana Image Renderer plugin configured, you can **Enable dashboards previews in pull requests**. If image rendering isn't 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. +### Modify your configuration after setup is complete + +To update your repository configuration after you've completed setup: + +1. Log in to your Grafana server with an account that has the Grafana Admin flag set. +1. Select **Administration** in the left-side menu and then **Provisioning**. +1. Select **Settings** for the repository you wish to modify. +1. Use the **Configure repository** screen to update any of the settings. +1. Select **Save** to preserve the updates. + +## Set up Git Sync as code + +Alternatively, you can also configure Git Sync using `grafanactl`. Since Git Sync configuration is managed as code using Custom Resource Definitions (CRDs), you can create a Repository CRD in a YAML file and use `grafanactl` to push it to Grafana. This approach enables automated, GitOps-style workflows for managing Git Sync configuration instead of using the Grafana UI. + +To set up Git Sync with `grafanactl`, follow these steps: + +1. [Create the repository CRD](#create-the-repository-crd) +1. [Push the repository CRD to Grafana](#push-the-repository-crd-to-grafana) +1. [Manage repository resources](#manage-repository-resources) +1. [Verify setup](#verify-setup) + +For more information, refer to the following documents: + +- [grafanactl Documentation](https://grafana.github.io/grafanactl/) +- [Repository CRD Reference](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-setup/) +- [Dashboard CRD Format](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/export-resources/) + +### Create the repository CRD + +Create a `repository.yaml` file defining your Git Sync configuration: + +```yaml +apiVersion: provisioning.grafana.app/v0alpha1 +kind: Repository +metadata: + name: +spec: + title: + type: github + github: + url: + branch: + path: grafana/ + generateDashboardPreviews: true + sync: + enabled: true + intervalSeconds: 60 + target: folder + workflows: + - write + - branch +secure: + token: + create: +``` + +Replace the placeholders with your values: + +- _``_: Unique identifier for this repository resource +- _``_: Human-readable name displayed in Grafana UI +- _``_: GitHub repository URL +- _``_: Branch to sync +- _``_: GitHub Personal Access Token + +{{< admonition type="note" >}} + +Only `target: folder` is currently supported for Git Sync. + +{{< /admonition >}} + +#### Configuration parameters + +The following configuration parameters are available: + +| Field | Description | +| --------------------------------------- | ----------------------------------------------------------- | +| `metadata.name` | Unique identifier for this repository resource | +| `spec.title` | Human-readable name displayed in Grafana UI | +| `spec.type` | Repository type (`github`) | +| `spec.github.url` | GitHub repository URL | +| `spec.github.branch` | Branch to sync | +| `spec.github.path` | Directory path containing dashboards | +| `spec.github.generateDashboardPreviews` | Generate preview images (true/false) | +| `spec.sync.enabled` | Enable synchronization (true/false) | +| `spec.sync.intervalSeconds` | Sync interval in seconds | +| `spec.sync.target` | Where to place synced dashboards (`folder`) | +| `spec.workflows` | Enabled workflows: `write` (direct commits), `branch` (PRs) | +| `secure.token.create` | GitHub Personal Access Token | + +### Push the repository CRD to Grafana + +Before pushing any resources, configure `grafanactl` with your Grafana instance details. Refer to the [grafanactl configuration documentation](https://grafana.github.io/grafanactl/) for setup instructions. + +Push the repository configuration: + +```sh +grafanactl resources push --path +``` + +The `--path` parameter has to point to the directory containing your `repository.yaml` file. + +After pushing, Grafana will: + +1. Create the repository resource +1. Connect to your GitHub repository +1. Pull dashboards from the specified path +1. Begin syncing at the configured interval + +### Manage repository resources + +#### List repositories + +To list all repositories: + +```sh +grafanactl resources get repositories +``` + +#### Get repository details + +To get details for a specific repository: + +```sh +grafanactl resources get repository/ +grafanactl resources get repository/ -o json +grafanactl resources get repository/ -o yaml +``` + +#### Update the repository + +To update a repository: + +```sh +grafanactl resources edit repository/ +``` + +#### Delete the repository + +To delete a repository: + +```sh +grafanactl resources delete repository/ +``` + +### Verify setup + +Check that Git Sync is working: + +```sh +# List repositories +grafanactl resources get repositories + +# Check Grafana UI +# Navigate to: Administration → Provisioning → Git Sync +``` + ## Verify your dashboards in Grafana To verify that your dashboards are available at the location that you specified, click **Dashboards**. The name of the dashboard is listed in the **Name** column. -Now that your dashboards have been synced from a repository, you can customize the name, change the branch, and create a pull request (PR) for it. Refer to [Manage provisioned repositories with Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/use-git-sync/) for more information. +Now that your dashboards have been synced from a repository, you can customize the name, change the branch, and create a pull request (PR) for it. Refer to [Manage provisioned repositories with Git Sync](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/use-git-sync/) for more information. -## Configure webhooks and image rendering +## Extend Git Sync for real-time notification and image rendering -You can extend Git Sync by getting instant updates and pull requests using webhooks and add dashboard previews in pull requests. +Optionally, you can extend Git Sync by enabling pull request notifications and image previews of dashboard changes. + +| Capability | Benefit | Requires | +| ----------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | +| Adds a table summarizing changes to your pull request | Provides a convenient way to save changes back to GitHub | Webhooks configured | +| Add a dashboard preview image to a PR | View a snapshot of dashboard changes to a pull request without opening Grafana | Image renderer and webhooks configured | ### Set up webhooks for realtime notification and pull request integration @@ -191,25 +350,26 @@ When connecting to a GitHub repository, Git Sync uses webhooks to enable real-ti 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`. -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. +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: -```yaml +```ini [server] -root_url = https://PUBLIC_DOMAIN.HERE +root_url = https:// ``` +Replace _``_ with your public domain. + To check the configured webhooks, go to **Administration** > **Provisioning** and click the **View** link for your GitHub repository. #### 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 doesn't 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, in RegExp: - `/apis/provisioning\.grafana\.app/v0(alpha1)?/namespaces/[^/]+/repositories/[^/]+/(webhook|render/.*)$` - ### Set up image rendering for dashboard previews @@ -217,12 +377,14 @@ Set up image rendering to add visual previews of dashboard updates directly in p 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 +## Next steps -To update your repository configuration after you've completed set up: +You've successfully set up Git Sync to manage your Grafana dashboards through version control. Your dashboards are now synchronized with a GitHub repository, enabling collaborative development and change tracking. -1. Log in to your Grafana server with an account that has the Grafana Admin flag set. -1. Select **Administration** in the left-side menu and then **Provisioning**. -1. Select **Settings** for the repository you wish to modify. -1. Use the **Configure repository** screen to update any of the settings. -1. Select **Save** to preserve the updates. +To learn more about using Git Sync: + +- [Work with provisioned dashboards](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/provisioned-dashboards/) +- [Manage provisioned repositories with Git Sync](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/use-git-sync/) +- [Git Sync deployment scenarios](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios) +- [Export resources](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/export-resources/) +- [grafanactl documentation](https://grafana.github.io/grafanactl/) diff --git a/docs/sources/as-code/observability-as-code/provision-resources/intro-git-sync.md b/docs/sources/as-code/observability-as-code/provision-resources/intro-git-sync.md index 4d9bda529a3..903df6303a6 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/intro-git-sync.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/intro-git-sync.md @@ -47,7 +47,7 @@ Using Git Sync, you can: {{< admonition type="caution" >}} -Git Sync only works with specific folders for the moment. Full-instance sync is not currently supported. +Full instance sync is not available in Grafana Cloud and is experimental and unsupported in Grafana OSS/Enterprise. {{< /admonition >}} @@ -84,7 +84,7 @@ Refer to [Requirements](https://grafana.com/docs/grafana//obser - 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 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/#choose-what-to-synchronize) for more details. +- Full-instance sync is not available in Grafana Cloud and is experimental in Grafana OSS and Grafana Enterprise. Refer to [Choose what to synchronize](../git-sync-setup/#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. @@ -127,7 +127,13 @@ An instance can be in one of the following Git Sync states: ## Common use cases -You can use Git Sync in the following scenarios. +{{< admonition type="note" >}} + +Refer to [Git Sync deployment scenarios](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/git-sync-deployment-scenarios) for sample scenarios, including architecture and configuration details. + +{{< /admonition >}} + +You can use Git Sync for the following use cases: ### Version control and auditing diff --git a/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md b/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md index f7d37d4b94e..11ce2aee0aa 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md @@ -14,7 +14,7 @@ labels: - cloud title: Manage provisioned repositories with Git Sync menuTitle: Manage repositories with Git Sync -weight: 120 +weight: 400 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/use-git-sync/ aliases: - ../../../observability-as-code/provision-resources/use-git-sync/ # /docs/grafana/next/observability-as-code/provision-resources/use-git-sync/ diff --git a/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md b/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md index 9598477354f..0ddc50376de 100644 --- a/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md +++ b/docs/sources/as-code/observability-as-code/schema-v2/links-schema.md @@ -62,5 +62,6 @@ The table includes default and other fields: | targetBlank | bool. If true, the link will be opened in a new tab. Default is `false`. | | includeVars | bool. If true, includes current template variables values in the link as query params. Default is `false`. | | keepTime | bool. If true, includes current time range in the link as query params. Default is `false`. | +| placement? | string. Use placement to display the link somewhere else on the dashboard other than above the visualizations. Use the `inControlsMenu` parameter to render the link in the dashboard controls dropdown menu. | diff --git a/docs/sources/datasources/_index.md b/docs/sources/datasources/_index.md index e7da61ea2f5..28d46c9c600 100644 --- a/docs/sources/datasources/_index.md +++ b/docs/sources/datasources/_index.md @@ -112,6 +112,12 @@ For example, this video demonstrates the visual Prometheus query builder: For general information about querying in Grafana, and common options and user interface elements across all query editors, refer to [Query and transform data](ref:query-transform-data). +## Build a dashboard from the data source + +After you've configured a data source, you can start creating a dashboard directly from it, by clicking the **Build a dashboard** button. + +For more information, refer to [Begin dashboard creation from data source configuration](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/create-dashboard/#begin-dashboard-creation-from-connections). + ## Special data sources Grafana includes three special data sources: diff --git a/docs/sources/datasources/azure-monitor/_index.md b/docs/sources/datasources/azure-monitor/_index.md index 90452f4cc52..d66b19efdff 100644 --- a/docs/sources/datasources/azure-monitor/_index.md +++ b/docs/sources/datasources/azure-monitor/_index.md @@ -3,7 +3,6 @@ aliases: - ../data-sources/azure-monitor/ - ../features/datasources/azuremonitor/ - azuremonitor/ - - azuremonitor/deprecated-application-insights/ description: Guide for using Azure Monitor in Grafana keywords: - grafana @@ -23,6 +22,7 @@ labels: menuTitle: Azure Monitor title: Azure Monitor data source weight: 300 +last_reviewed: 2025-12-04 refs: configure-grafana-feature-toggles: - pattern: /docs/grafana/ @@ -49,6 +49,11 @@ refs: destination: /docs/grafana//dashboards/build-dashboards/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//dashboards/build-dashboards/ + transform-data: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/query-transform-data/transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//panels-visualizations/query-transform-data/transform-data/ configure-grafana-azure: - pattern: /docs/grafana/ destination: /docs/grafana//setup-grafana/configure-grafana/#azure @@ -63,295 +68,98 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + configure-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + query-editor-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + template-variables-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + alerting-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/alerting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/alerting/ + troubleshooting-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ + annotations-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/annotations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/annotations/ --- # Azure Monitor data source -Grafana ships with built-in support for Azure Monitor, the Azure service to maximize the availability and performance of applications and services in the Azure Cloud. -This topic explains configuring and querying specific to the Azure Monitor data source. +The Azure Monitor data source plugin allows you to query and visualize data from Azure Monitor, the Azure service to maximize the availability and performance of applications and services in the Azure Cloud. -For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:data-source-management). -Only users with the organization administrator role can add data sources. +## Supported Azure clouds -Once you've added the Azure Monitor data source, you can [configure it](#configure-the-data-source) so that your Grafana instance's users can create queries in its [query editor](query-editor/) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore). +The Azure Monitor data source supports the following Azure cloud environments: -The Azure Monitor data source supports visualizing data from four Azure services: +- **Azure** - Azure public cloud (default) +- **Azure US Government** - Azure Government cloud +- **Azure China** - Azure China cloud operated by 21Vianet -- **Azure Monitor Metrics:** Collect numeric data from resources in your Azure account. -- **Azure Monitor Logs:** Collect log and performance data from your Azure account, and query using the Kusto Query Language (KQL). -- **Azure Resource Graph:** Query your Azure resources across subscriptions. -- **Azure Monitor Application Insights:** Collect trace logging data and other application performance metrics. +## Supported Azure services -## Configure the data source +The Azure Monitor data source supports the following Azure services: -**To access the data source configuration page:** +| Service | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| **Azure Monitor Metrics** | Collect numeric data from resources in your Azure account. Supports dimensions, aggregations, and time grain configuration. | +| **Azure Monitor Logs** | Collect log and performance data from your Azure account using the Kusto Query Language (KQL). | +| **Azure Resource Graph** | Query your Azure resources across subscriptions using KQL. Useful for inventory, compliance, and resource management. | +| **Application Insights Traces** | Collect distributed trace data and correlate requests across your application components. | -1. Click **Connections** in the left-side menu. -1. Under Your connections, click **Data sources**. -1. Enter `Azure Monitor` in the search bar. -1. Click **Azure Monitor**. +## Get started - The **Settings** tab of the data source is displayed. +The following documents will help you get started with the Azure Monitor data source: -### Configure Azure Active Directory (AD) authentication +- [Configure the Azure Monitor data source](ref:configure-azure-monitor) - Set up authentication and connect to Azure +- [Azure Monitor query editor](ref:query-editor-azure-monitor) - Create and edit queries for Metrics, Logs, Traces, and Resource Graph +- [Template variables](ref:template-variables-azure-monitor) - Create dynamic dashboards with Azure Monitor variables +- [Alerting](ref:alerting-azure-monitor) - Create alert rules using Azure Monitor data +- [Troubleshooting](ref:troubleshooting-azure-monitor) - Solve common configuration and query errors -You must create an app registration and service principal in Azure AD to authenticate the data source. -For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in). +## Additional features -The app registration you create must have the `Reader` role assigned on the subscription. -For more information, refer to [Azure documentation for role assignments](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current). +After you have configured the Azure Monitor data source, you can: -If you host Grafana in Azure, such as in App Service or Azure Virtual Machines, you can configure the Azure Monitor data source to use Managed Identity for secure authentication without entering credentials into Grafana. -For details, refer to [Configuring using Managed Identity](#configuring-using-managed-identity). +- Add [Annotations](ref:annotations-azure-monitor) to overlay Azure log events on your graphs. +- Configure and use [Template variables](ref:template-variables-azure-monitor) for dynamic dashboards. +- Add [Transformations](ref:transform-data) to manipulate query results. +- Set up [Alerting](ref:alerting-azure-monitor) and recording rules using Metrics, Logs, Traces, and Resource Graph queries. +- Use [Explore](ref:explore) to investigate your Azure data without building a dashboard. -You can configure the Azure Monitor data source to use Workload Identity for secure authentication without entering credentials into Grafana if you host Grafana in a Kubernetes environment, such as AKS, and require access to Azure resources. -For details, refer to [Configuring using Workload Identity](#configuring-using-workload-identity). +## Pre-built dashboards -| Name | Description | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Authentication** | Enables Managed Identity. Selecting Managed Identity hides many of the other fields. For details, see [Configuring using Managed Identity](#configuring-using-managed-identity). | -| **Azure Cloud** | Sets the national cloud for your Azure account. For most users, this is the default "Azure". For details, see the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud). | -| **Directory (tenant) ID** | Sets the directory/tenant ID for the Azure AD app registration to use for authentication. For details, see the [Azure tenant and app ID docs](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in). | -| **Application (client) ID** | Sets the application/client ID for the Azure AD app registration to use for authentication. | -| **Client secret** | Sets the application client secret for the Azure AD app registration to use for authentication. For details, see the [Azure application secret docs](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret). | -| **Default subscription** | _(Optional)_ Sets a default subscription for template variables to use. | -| **Enable Basic Logs** | Allows this data source to execute queries against [Basic Logs tables](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-query?tabs=portal-1) in supported Log Analytics Workspaces. These queries may incur additional costs. | +The Azure Monitor plugin includes the following pre-built dashboards: -### Provision the data source +- **Azure Monitor Overview** - Displays key metrics across your Azure subscriptions and resources. +- **Azure Storage Account** - Shows storage account metrics including availability, latency, and transactions. -You can define and configure the data source in YAML files as part of Grafana's provisioning system. -For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources). +To import a pre-built dashboard: -#### Provisioning examples +1. Go to **Connections** > **Data sources**. +1. Select your Azure Monitor data source. +1. Click the **Dashboards** tab. +1. Click **Import** next to the dashboard you want to use. -**Azure AD App Registration (client secret):** +## Related resources -```yaml -apiVersion: 1 # config file version - -datasources: - - name: Azure Monitor - type: grafana-azure-monitor-datasource - access: proxy - jsonData: - azureAuthType: clientsecret - cloudName: azuremonitor # See table below - tenantId: - clientId: - subscriptionId: # Optional, default subscription - secureJsonData: - clientSecret: - version: 1 -``` - -**Managed Identity:** - -```yaml -apiVersion: 1 # config file version - -datasources: - - name: Azure Monitor - type: grafana-azure-monitor-datasource - access: proxy - jsonData: - azureAuthType: msi - subscriptionId: # Optional, default subscription - version: 1 -``` - -**Workload Identity:** - -```yaml -apiVersion: 1 # config file version - -datasources: - - name: Azure Monitor - type: grafana-azure-monitor-datasource - access: proxy - jsonData: - azureAuthType: workloadidentity - subscriptionId: # Optional, default subscription - version: 1 -``` - -**Current User:** - -{{< admonition type="note" >}} -The `oauthPassThru` property is required for current user authentication to function. -Additionally, `disableGrafanaCache` is necessary to prevent the data source returning cached responses for resources users don't have access to. -{{< /admonition >}} - -```yaml -apiVersion: 1 # config file version - -datasources: - - name: Azure Monitor - type: grafana-azure-monitor-datasource - access: proxy - jsonData: - azureAuthType: currentuser - oauthPassThru: true - disableGrafanaCache: true - subscriptionId: # Optional, default subscription - version: 1 -``` - -#### Supported cloud names - -| Azure Cloud | `cloudName` Value | -| ------------------------------------ | -------------------------- | -| **Microsoft Azure public cloud** | `azuremonitor` (_Default_) | -| **Microsoft Chinese national cloud** | `chinaazuremonitor` | -| **US Government cloud** | `govazuremonitor` | - -{{< admonition type="note" >}} -Cloud names for current user authentication differ to the `cloudName` values in the preceding table. -The public cloud name is `AzureCloud`, the Chinese national cloud name is `AzureChinaCloud`, and the US Government cloud name is `AzureUSGovernment`. -{{< /admonition >}} - -### Configure Managed Identity - -{{< admonition type="note" >}} -Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud. -{{< /admonition >}} - -You can use managed identity to configure Azure Monitor in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM. -This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations. -For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). - -**To enable managed identity for Grafana:** - -1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). - - ```ini - [azure] - managed_identity_enabled = true - ``` - -2. In the Azure Monitor data source configuration, set **Authentication** to **Managed Identity**. - - This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to Azure Monitor Metrics and Logs, and Azure Resource Graph. - - {{< figure src="/media/docs/grafana/data-sources/screenshot-managed-identity-2.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Managed Identity authentication" >}} - -3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity. - -```ini -[azure] -managed_identity_enabled = true -managed_identity_client_id = USER_ASSIGNED_IDENTITY_CLIENT_ID -``` - -### Configure Workload Identity - -You can use workload identity to configure Azure Monitor in Grafana if you host Grafana in a Kubernetes environment, such as AKS, in conjunction with managed identities. -This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations. -For details on workload identity, refer to the [Azure workload identity documentation](https://azure.github.io/azure-workload-identity/docs/). - -**To enable workload identity for Grafana:** - -1. Set the `workload_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). - - ```ini - [azure] - workload_identity_enabled = true - ``` - -2. In the Azure Monitor data source configuration, set **Authentication** to **Workload Identity**. - - This hides the directory ID, application ID, and client secret fields, and the data source uses workload identity to authenticate to Azure Monitor Metrics and Logs, and Azure Resource Graph. - - {{< figure src="/media/docs/grafana/data-sources/screenshot-workload-identity.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Workload Identity authentication" >}} - -3. There are additional configuration variables that can control the authentication method.`workload_identity_tenant_id` represents the Azure AD tenant that contains the managed identity, `workload_identity_client_id` represents the client ID of the managed identity if it differs from the default client ID, `workload_identity_token_file` represents the path to the token file. Refer to the [documentation](https://azure.github.io/azure-workload-identity/docs/) for more information on what values these variables should use, if any. - - ```ini - [azure] - workload_identity_enabled = true - workload_identity_tenant_id = IDENTITY_TENANT_ID - workload_identity_client_id = IDENTITY_CLIENT_ID - workload_identity_token_file = TOKEN_FILE_PATH - ``` - -### Configure Current User authentication - -{{< admonition type="note" >}} -Current user authentication is an [experimental feature](/docs/release-life-cycle). Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Contact Grafana Support to enable this feature in Grafana Cloud. Aspects of Grafana may not work as expected when using this authentication method. -{{< /admonition >}} - -If your Grafana instance is configured with Azure Entra (formerly Active Directory) authentication for login, this authentication method can be used to forward the currently logged in user's credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation](ref:configure-grafana-azure-auth). - -{{< admonition type="note" >}} -Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source. - -The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**. - -1. In the Azure Portal, open the App Registration that requires configuration. -2. Select **Authentication** in the side menu. -3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes. -4. Save the changes to ensure the App Registration is updated. - -The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source. - -1. In the Azure Portal, open the App Registration that requires configuration. -1. Select **API Permissions** in the side menu. -1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added. -1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information. - - Select **Azure Service Management** > **Delegated permissions** > `user_impersonation` > **Add permissions** - - Select **APIs my organization uses** > Search for **Log Analytics API** and select it > **Delegated permissions** > `Date.Read` > **Add permissions** - -Once all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. Once updated the scopes value should equal: `.default openid email profile`. -{{< /admonition >}} - -This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. -Affected functionality includes alerting, reporting, and recorded queries. -In order to support backend queries when using a data source configured with current user authentication, you can configure service credentials. -Also, note that query and resource caching is disabled by default for data sources using current user authentication. - -{{< admonition type="note" >}} -To configure fallback service credentials the [feature toggle](ref:configure-grafana-feature-toggles) `idForwarding` must be set to `true` and `user_identity_fallback_credentials_enabled` must be enabled in the [Azure configuration section](ref:configure-grafana-azure) (enabled by default when `user_identity_enabled` is set to `true`). -{{< /admonition >}} - -Permissions for fallback credentials may need to be broad to appropriately support backend functionality. -For example, an alerting query created by a user is dependent on their permissions. -If a user tries to create an alert for a resource that the fallback credentials can't access, the alert will fail. - -**To enable current user authentication for Grafana:** - -1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). - By default this will also enable fallback service credentials. - If you want to disable service credentials at the instance level set `user_identity_fallback_credentials_enabled` to false. - - ```ini - [azure] - user_identity_enabled = true - ``` - -1. In the Azure Monitor data source configuration, set **Authentication** to **Current User**. - If fallback service credentials are enabled at the instance level, an additional configuration section is visible that you can use to enable or disable using service credentials for this data source. - {{< figure src="/media/docs/grafana/data-sources/screenshot-current-user.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Current User authentication" >}} - -1. If you want backend functionality to work with this data source, enable service credentials and configure the data source using the most applicable credentials for your circumstances. - -## Query the data source - -The Azure Monitor data source can query data from Azure Monitor Metrics and Logs, the Azure Resource Graph, and Application Insights Traces. Each source has its own specialized query editor. - -For details, see the [query editor documentation](query-editor/). - -## Use template variables - -Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. -Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard. -Grafana refers to such variables as template variables. - -For details, see the [template variables documentation](template-variables/). - -## Application Insights and Insights Analytics (removed) - -Until Grafana v8.0, you could query the same Azure Application Insights data using Application Insights and Insights Analytics. - -These queries were deprecated in Grafana v7.5. In Grafana v8.0, Application Insights and Insights Analytics were made read-only in favor of querying this data through Metrics and Logs. These query methods were completely removed in Grafana v9.0. - -If you're upgrading from a Grafana version prior to v9.0 and relied on Application Insights and Analytics queries, refer to the [Grafana v9.0 documentation](/docs/grafana/v9.0/datasources/azuremonitor/deprecated-application-insights/) for help migrating these queries to Metrics and Logs queries. +- [Azure Monitor documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/) +- [Kusto Query Language (KQL) reference](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/) +- [Grafana community forum](https://community.grafana.com/) diff --git a/docs/sources/datasources/azure-monitor/alerting/index.md b/docs/sources/datasources/azure-monitor/alerting/index.md new file mode 100644 index 00000000000..860c1d343a4 --- /dev/null +++ b/docs/sources/datasources/azure-monitor/alerting/index.md @@ -0,0 +1,262 @@ +--- +aliases: + - ../../data-sources/azure-monitor/alerting/ +description: Set up alerts using Azure Monitor data in Grafana +keywords: + - grafana + - azure + - monitor + - alerting + - alerts + - metrics + - logs +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Alerting +title: Azure Monitor alerting +weight: 500 +refs: + alerting: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//alerting/ + alerting-fundamentals: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//alerting/fundamentals/ + create-alert-rule: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/ + grafana-managed-recording-rules: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules/ + configure-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + query-editor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + troubleshoot: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ +--- + +# Azure Monitor alerting + +The Azure Monitor data source supports [Grafana Alerting](ref:alerting) and [Grafana-managed recording rules](ref:grafana-managed-recording-rules), allowing you to create alert rules based on Azure metrics, logs, traces, and resource data. You can monitor your Azure environment and receive notifications when specific conditions are met. + +## Before you begin + +- Ensure you have the appropriate permissions to create alert rules in Grafana. +- Verify your Azure Monitor data source is configured and working correctly. +- Familiarize yourself with [Grafana Alerting concepts](ref:alerting-fundamentals). +- **Important**: Verify your data source uses a supported authentication method. Refer to [Authentication requirements](#authentication-requirements). + +## Supported query types for alerting + +All Azure Monitor query types support alerting and recording rules: + +| Query type | Use case | Notes | +| -------------------- | -------------------------------------------------- | -------------------------------------------------------- | +| Metrics | Threshold-based alerts on Azure resource metrics | Best suited for alerting; returns time-series data | +| Logs | Alert on log patterns, error counts, or thresholds | Use KQL to aggregate data into numeric values | +| Azure Resource Graph | Alert on resource state or configuration changes | Use count aggregations to return numeric data | +| Traces | Alert on trace data and application performance | Use aggregations to return numeric values for evaluation | + +{{< admonition type="note" >}} +Alert queries must return numeric data that Grafana can evaluate against a threshold. Queries that return only text or non-numeric data cannot be used directly for alerting. +{{< /admonition >}} + +## Authentication requirements + +Alerting and recording rules run as background processes without a user context. This means they require service-level authentication and don't work with all authentication methods. + +| Authentication method | Supported | +| -------------------------------- | ------------------------------------- | +| App Registration (client secret) | ✓ | +| Managed Identity | ✓ | +| Workload Identity | ✓ | +| Current User | ✓ (with fallback service credentials) | + +{{< admonition type="note" >}} +If you use **Current User** authentication, you must configure **fallback service credentials** for alerting and recording rules to function. User credentials aren't available for background operations, so Grafana uses the fallback credentials instead. Refer to [configure the data source](ref:configure-azure-monitor) for details on setting up fallback credentials. +{{< /admonition >}} + +## Create an alert rule + +To create an alert rule using Azure Monitor data: + +1. Go to **Alerting** > **Alert rules**. +1. Click **New alert rule**. +1. Enter a name for your alert rule. +1. In the **Define query and alert condition** section: + - Select your Azure Monitor data source. + - Configure your query (for example, a Metrics query for CPU usage or a Logs query using KQL). + - Add a **Reduce** expression if your query returns multiple series. + - Add a **Threshold** expression to define the alert condition. +1. Configure the **Set evaluation behavior**: + - Select or create a folder and evaluation group. + - Set the evaluation interval (how often the alert is checked). + - Set the pending period (how long the condition must be true before firing). +1. Add labels and annotations to provide context for notifications. +1. Click **Save rule**. + +For detailed instructions, refer to [Create a Grafana-managed alert rule](ref:create-alert-rule). + +## Example: VM CPU usage alert + +This example creates an alert that fires when virtual machine CPU usage exceeds 80%: + +1. Create a new alert rule. +1. Configure the query: + - **Service**: Metrics + - **Resource**: Select your virtual machine + - **Metric namespace**: `Microsoft.Compute/virtualMachines` + - **Metric**: `Percentage CPU` + - **Aggregation**: `Average` +1. Add expressions: + - **Reduce**: Last (to get the most recent data point) + - **Threshold**: Is above 80 +1. Set evaluation to run every 1 minute with a 5-minute pending period. +1. Save the rule. + +## Example: Error log count alert + +This example alerts when error logs exceed a threshold using a KQL query: + +1. Create a new alert rule. +1. Configure the query: + - **Service**: Logs + - **Resource**: Select your Log Analytics workspace + - **Query**: + ```kusto + AppExceptions + | where TimeGenerated > ago(5m) + | summarize ErrorCount = count() by bin(TimeGenerated, 1m) + ``` +1. Add expressions: + - **Reduce**: Max (to get the highest count in the period) + - **Threshold**: Is above 10 +1. Set evaluation to run every 5 minutes. +1. Save the rule. + +## Example: Resource count alert + +This example alerts when the number of running virtual machines drops below a threshold using Azure Resource Graph: + +1. Create a new alert rule. +1. Configure the query: + - **Service**: Azure Resource Graph + - **Subscriptions**: Select your subscriptions + - **Query**: + + ```kusto + resources + | where type == "microsoft.compute/virtualmachines" + | where properties.extended.instanceView.powerState.displayStatus == "VM running" + | summarize RunningVMs = count() + ``` + +1. Add expressions: + - **Reduce**: Last + - **Threshold**: Is below 3 +1. Set evaluation to run every 5 minutes. +1. Save the rule. + +## Best practices + +Follow these recommendations to create reliable and efficient alerts with Azure Monitor data. + +### Use appropriate query intervals + +- Set the alert evaluation interval to be greater than or equal to the minimum data resolution from Azure Monitor. +- Azure Monitor Metrics typically have 1-minute granularity at minimum. +- Avoid very short intervals (less than 1 minute) as they may cause evaluation timeouts or miss data points. + +### Reduce multiple series + +When your Azure Monitor query returns multiple time series (for example, CPU usage across multiple VMs), use the **Reduce** expression to aggregate them: + +- **Last**: Use the most recent value +- **Mean**: Average across all series +- **Max/Min**: Use the highest or lowest value +- **Sum**: Total across all series + +### Optimize Log Analytics queries + +For Logs queries used in alerting: + +- Use `summarize` to aggregate data into numeric values. +- Include appropriate time filters using `ago()` or `TimeGenerated`. +- Avoid returning large result sets; aggregate data in the query. +- Test queries in Explore before using them in alert rules. + +### Handle no data conditions + +Configure what happens when no data is returned: + +1. In the alert rule, find **Configure no data and error handling**. +1. Choose an appropriate action: + - **No Data**: Keep the alert in its current state + - **Alerting**: Treat no data as an alert condition + - **OK**: Treat no data as a healthy state + +### Test queries before alerting + +Always verify your query returns expected data before creating an alert: + +1. Go to **Explore**. +1. Select your Azure Monitor data source. +1. Run the query you plan to use for alerting. +1. Confirm the data format and values are correct. +1. Verify the query returns numeric data suitable for threshold evaluation. + +## Troubleshooting + +If your Azure Monitor alerts aren't working as expected, use the following sections to diagnose and resolve common issues. + +### Alerts not firing + +- Verify the data source uses a supported authentication method. If using Current User authentication, ensure fallback service credentials are configured. +- Check that the query returns numeric data in Explore. +- Ensure the evaluation interval allows enough time for data to be available. +- Review the alert rule's health and any error messages in the Alerting UI. + +### Authentication errors in alert evaluation + +If you see authentication errors when alerts evaluate: + +- Confirm the data source is configured with App Registration, Managed Identity, Workload Identity, or Current User with fallback service credentials. +- If using App Registration, verify the client secret hasn't expired. +- If using Current User, verify that fallback service credentials are configured and valid. +- Check that the service principal has appropriate permissions on Azure resources. + +### Query timeout errors + +- Simplify complex KQL queries. +- Reduce the time range in Log Analytics queries. +- Add more specific filters to narrow result sets. + +For additional troubleshooting help, refer to [Troubleshoot Azure Monitor](ref:troubleshoot). + +## Additional resources + +- [Grafana Alerting documentation](ref:alerting) +- [Create alert rules](ref:create-alert-rule) +- [Azure Monitor query editor](ref:query-editor) +- [Grafana-managed recording rules](ref:grafana-managed-recording-rules) diff --git a/docs/sources/datasources/azure-monitor/annotations/index.md b/docs/sources/datasources/azure-monitor/annotations/index.md new file mode 100644 index 00000000000..43fbb914a9d --- /dev/null +++ b/docs/sources/datasources/azure-monitor/annotations/index.md @@ -0,0 +1,218 @@ +--- +aliases: + - ../../data-sources/azure-monitor/annotations/ +description: Use annotations with the Azure Monitor data source in Grafana +keywords: + - grafana + - azure + - monitor + - annotations + - events + - logs +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Annotations +title: Azure Monitor annotations +weight: 450 +refs: + annotate-visualizations: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ + query-editor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ +--- + +# Azure Monitor annotations + +[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs. You can use Azure Monitor Log Analytics queries to create annotations that mark important events, deployments, alerts, or other significant occurrences on your dashboards. + +## Before you begin + +- Ensure you have configured the Azure Monitor data source. +- You need access to a Log Analytics workspace containing the data you want to use for annotations. +- Annotations use Log Analytics (KQL) queries only. Metrics, Traces, and Azure Resource Graph queries are not supported for annotations. + +## Create an annotation query + +To add an Azure Monitor annotation to a dashboard: + +1. Open the dashboard where you want to add annotations. +1. Click **Dashboard settings** (gear icon) in the top navigation. +1. Select **Annotations** in the left menu. +1. Click **Add annotation query**. +1. Enter a **Name** for the annotation (e.g., "Azure Activity", "Deployments"). +1. Select your **Azure Monitor** data source. +1. Choose the **Logs** service. +1. Select a **Resource** (Log Analytics workspace or Application Insights resource). +1. Write a KQL query that returns the annotation data. +1. Click **Apply** to save. + +## Query requirements + +Your KQL query should return columns that Grafana can use to create annotations: + +| Column | Required | Description | +| ------------------ | ----------- | ------------------------------------------------------------------------------------------------ | +| `TimeGenerated` | Yes | The timestamp for the annotation. Grafana uses this to position the annotation on the time axis. | +| `Text` | Recommended | The annotation text displayed when you hover over or click the annotation. | +| Additional columns | Optional | Any other columns returned become annotation tags. | + +{{< admonition type="note" >}} +Always include a time filter in your query to limit results to the dashboard's time range. Use the `$__timeFilter()` macro. +{{< /admonition >}} + +## Annotation query examples + +The following examples demonstrate common annotation use cases. + +### Azure Activity Log events + +Display Azure Activity Log events such as resource modifications, deployments, and administrative actions: + +```kusto +AzureActivity +| where $__timeFilter(TimeGenerated) +| where Level == "Error" or Level == "Warning" or CategoryValue == "Administrative" +| project TimeGenerated, Text=OperationNameValue, Level, ResourceGroup, Caller +| order by TimeGenerated desc +| take 100 +``` + +### Deployment events + +Show deployment-related activity: + +```kusto +AzureActivity +| where $__timeFilter(TimeGenerated) +| where OperationNameValue contains "deployments" +| project TimeGenerated, Text=strcat("Deployment: ", OperationNameValue), Status=ActivityStatusValue, ResourceGroup +| order by TimeGenerated desc +``` + +### Application Insights exceptions + +Mark application exceptions as annotations: + +```kusto +AppExceptions +| where $__timeFilter(TimeGenerated) +| project TimeGenerated, Text=strcat(ProblemId, ": ", OuterMessage), SeverityLevel, AppRoleName +| order by TimeGenerated desc +| take 50 +``` + +### Custom events from Application Insights + +Display custom events logged by your application: + +```kusto +AppEvents +| where $__timeFilter(TimeGenerated) +| where Name == "DeploymentStarted" or Name == "DeploymentCompleted" +| project TimeGenerated, Text=Name, AppRoleName +| order by TimeGenerated desc +``` + +### Security alerts + +Show security-related alerts: + +```kusto +SecurityAlert +| where $__timeFilter(TimeGenerated) +| project TimeGenerated, Text=AlertName, Severity=AlertSeverity, Description +| order by TimeGenerated desc +| take 50 +``` + +### Resource health events + +Display resource health status changes: + +```kusto +AzureActivity +| where $__timeFilter(TimeGenerated) +| where CategoryValue == "ResourceHealth" +| project TimeGenerated, Text=OperationNameValue, Status=ActivityStatusValue, ResourceId +| order by TimeGenerated desc +``` + +### VM start and stop events + +Mark virtual machine state changes: + +```kusto +AzureActivity +| where $__timeFilter(TimeGenerated) +| where OperationNameValue has_any ("start", "deallocate", "restart") +| where ResourceProviderValue == "MICROSOFT.COMPUTE" +| project TimeGenerated, Text=OperationNameValue, VM=Resource, Status=ActivityStatusValue +| order by TimeGenerated desc +``` + +### Autoscale events + +Show autoscale operations: + +```kusto +AzureActivity +| where $__timeFilter(TimeGenerated) +| where OperationNameValue contains "autoscale" +| project TimeGenerated, Text=strcat("Autoscale: ", OperationNameValue), Status=ActivityStatusValue, ResourceGroup +| order by TimeGenerated desc +``` + +## Customize annotation appearance + +After creating an annotation query, you can customize its appearance: + +| Setting | Description | +| ------------- | -------------------------------------------------------------------------------------------------------- | +| **Color** | Choose a color for the annotation markers. Use different colors to distinguish between annotation types. | +| **Show in** | Select which panels display the annotations. | +| **Filter by** | Add filters to limit when annotations appear. | + +## Best practices + +Follow these recommendations when creating annotations: + +1. **Limit results**: Always use `take` or `limit` to restrict the number of annotations. Too many annotations can clutter your dashboard and impact performance. + +2. **Use time filters**: Include `$__timeFilter()` to ensure queries only return data within the dashboard's time range. + +3. **Create meaningful text**: Use `strcat()` or `project` to create descriptive annotation text that provides context at a glance. + +4. **Add relevant tags**: Include columns like `ResourceGroup`, `Severity`, or `Status` that become clickable tags for filtering. + +5. **Use descriptive names**: Name your annotations clearly (e.g., "Production Deployments", "Critical Alerts") so dashboard users understand what they represent. + +## Troubleshoot annotations + +If annotations aren't appearing as expected, try the following solutions. + +### Annotations don't appear + +- Verify the query returns data in the selected time range. +- Check that the query includes a `TimeGenerated` column. +- Test the query in the Azure Portal Log Analytics query editor. +- Ensure the annotation is enabled (toggle is on). + +### Too many annotations + +- Add more specific filters to your query. +- Use `take` to limit results. +- Narrow the time range. + +### Annotations appear at wrong times + +- Verify the `TimeGenerated` column contains the correct timestamp. +- Check your dashboard's timezone settings. diff --git a/docs/sources/datasources/azure-monitor/configure/index.md b/docs/sources/datasources/azure-monitor/configure/index.md new file mode 100644 index 00000000000..cef21b08744 --- /dev/null +++ b/docs/sources/datasources/azure-monitor/configure/index.md @@ -0,0 +1,605 @@ +--- +aliases: + - ../../data-sources/azure-monitor/configure/ +description: Guide for configuring the Azure Monitor data source in Grafana. +keywords: + - grafana + - microsoft + - azure + - monitor + - application + - insights + - log + - analytics + - guide +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Configure +title: Configure the Azure Monitor data source +weight: 200 +last_reviewed: 2025-12-04 +refs: + configure-grafana-feature-toggles: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/#feature_toggles + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/#feature_toggles + provisioning-data-sources: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/provisioning/#data-sources + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/provisioning/#data-sources + explore: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//explore/ + configure-grafana-azure-auth: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/ + build-dashboards: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//dashboards/build-dashboards/ + configure-grafana-azure: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + data-source-management: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/data-source-management/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/data-source-management/ + configure-grafana-azure-auth-scopes: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + data-sources: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/ + private-data-source-connect: + - pattern: /docs/grafana/ + destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + - pattern: /docs/grafana-cloud/ + destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + configure-pdc: + - pattern: /docs/grafana/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc +--- + +# Configure the Azure Monitor data source + +This document explains how to configure the Azure Monitor data source and the available configuration options. +For general information about data sources, refer to [Grafana data sources](ref:data-sources) and [Data source management](ref:data-source-management). + +## Before you begin + +Before configuring the Azure Monitor data source, ensure you have the following: + +- **Grafana permissions:** You must have the `Organization administrator` role to configure data sources. + Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system or [using Terraform](#configure-with-terraform). + +- **Azure prerequisites:** Depending on your chosen authentication method, you may need: + - A Microsoft Entra ID (formerly Azure AD) app registration with a service principal (for App Registration authentication) + - A Managed Identity enabled on your Azure VM or App Service (for Managed Identity authentication) + - Workload identity configured in your Kubernetes cluster (for Workload Identity authentication) + - Microsoft Entra ID authentication configured for Grafana login (for Current User authentication) + +{{< admonition type="note" >}} +**Grafana Cloud users:** Managed Identity and Workload Identity authentication methods are not available in Grafana Cloud because they require Grafana to run on your Azure infrastructure. Use **App Registration** authentication instead. +{{< /admonition >}} + +- **Azure RBAC permissions:** The identity used to authenticate must have the `Reader` role on the Azure subscription containing the resources you want to monitor. + For Log Analytics queries, the identity also needs appropriate permissions on the Log Analytics workspaces to be queried. + Refer to the [Azure documentation for role assignments](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current). + +{{< admonition type="note" >}} +The Azure Monitor data source plugin is built into Grafana. No additional installation is required. +{{< /admonition >}} + +## Add the data source + +To add the Azure Monitor data source: + +1. Click **Connections** in the left-side menu. +1. Click **Add new connection**. +1. Type `Azure Monitor` in the search bar. +1. Select **Azure Monitor**. +1. Click **Add new data source** in the upper right. + +You're taken to the **Settings** tab where you can configure the data source. + +## Choose an authentication method + +The Azure Monitor data source supports four authentication methods. Choose based on where Grafana is hosted and your security requirements: + +| Authentication method | Best for | Requirements | +| --------------------- | ------------------------------------------ | -------------------------------------------------------------- | +| **App Registration** | Any Grafana deployment | Microsoft Entra ID app registration with client secret | +| **Managed Identity** | Grafana hosted in Azure (VMs, App Service) | Managed identity enabled on the Azure resource | +| **Workload Identity** | Grafana in Kubernetes (AKS) | Workload identity federation configured | +| **Current User** | User-level access control | Microsoft Entra ID authentication configured for Grafana login | + +## Configure authentication + +Select one of the following authentication methods and complete the configuration. + +### App Registration + +Use a Microsoft Entra ID app registration (service principal) to authenticate. This method works with any Grafana deployment. + +#### App Registration prerequisites + +1. Create an app registration in Microsoft Entra ID. + Refer to the [Azure documentation for creating a service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in). + +1. Create a client secret for the app registration. + Refer to the [Azure documentation for creating a client secret](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret). + +1. Assign the `Reader` role to the app registration on the subscription or resources you want to monitor. + Refer to the [Azure documentation for role assignments](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current). + +#### App Registration UI configuration + +| Setting | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| **Authentication** | Select **App Registration**. | +| **Azure Cloud** | The Azure environment to connect to. Select **Azure** for the public cloud, or choose Azure Government or Azure China for national clouds. | +| **Directory (tenant) ID** | The GUID that identifies your Microsoft Entra ID tenant. | +| **Application (client) ID** | The GUID for the app registration you created. | +| **Client secret** | The secret key for the app registration. Keep this secure and rotate periodically. | +| **Default Subscription** | Click **Load Subscriptions** to populate available subscriptions, then select your default. | + +#### Provision App Registration with YAML + +```yaml +apiVersion: 1 + +datasources: + - name: Azure Monitor + type: grafana-azure-monitor-datasource + access: proxy + jsonData: + azureAuthType: clientsecret + cloudName: azuremonitor # See supported cloud names below + tenantId: + clientId: + subscriptionId: # Optional, default subscription + secureJsonData: + clientSecret: + version: 1 +``` + +### Managed Identity + +Use Azure Managed Identity for secure, credential-free authentication when Grafana is hosted in Azure. + +{{< admonition type="note" >}} +Managed Identity is available in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or self-hosted Grafana deployed in Azure. It is not available in Grafana Cloud. +{{< /admonition >}} + +#### Managed Identity prerequisites + +- Grafana must be hosted in Azure (App Service, Azure VMs, or Azure Managed Grafana). +- Managed identity must be enabled on the Azure resource hosting Grafana. +- The managed identity must have the `Reader` role on the subscription or resources you want to monitor. + +For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). + +#### Managed Identity Grafana server configuration + +Enable managed identity in the Grafana server configuration: + +```ini +[azure] +managed_identity_enabled = true +``` + +To use a user-assigned managed identity instead of the system-assigned identity, also set: + +```ini +[azure] +managed_identity_enabled = true +managed_identity_client_id = +``` + +Refer to [Grafana Azure configuration](ref:configure-grafana-azure) for more details. + +#### Managed Identity UI configuration + +| Setting | Description | +| ------------------------ | --------------------------------------------------------------------------------------------------- | +| **Authentication** | Select **Managed Identity**. The directory ID, application ID, and client secret fields are hidden. | +| **Default Subscription** | Click **Load Subscriptions** to populate available subscriptions, then select your default. | + +{{< figure src="/media/docs/grafana/data-sources/screenshot-managed-identity-2.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor data source configured with Managed Identity" >}} + +#### Provision Managed Identity with YAML + +```yaml +apiVersion: 1 + +datasources: + - name: Azure Monitor + type: grafana-azure-monitor-datasource + access: proxy + jsonData: + azureAuthType: msi + subscriptionId: # Optional, default subscription + version: 1 +``` + +### Workload Identity + +Use Azure Workload Identity for secure authentication in Kubernetes environments like AKS. + +#### Workload Identity prerequisites + +- Grafana must be running in a Kubernetes environment with workload identity federation configured. +- The workload identity must have the `Reader` role on the subscription or resources you want to monitor. + +For details, refer to the [Azure workload identity documentation](https://azure.github.io/azure-workload-identity/docs/). + +#### Workload Identity Grafana server configuration + +Enable workload identity in the Grafana server configuration: + +```ini +[azure] +workload_identity_enabled = true +``` + +Optional configuration variables: + +```ini +[azure] +workload_identity_enabled = true +workload_identity_tenant_id = # Microsoft Entra ID tenant containing the managed identity +workload_identity_client_id = # Client ID if different from default +workload_identity_token_file = # Path to the token file +``` + +Refer to [Grafana Azure configuration](ref:configure-grafana-azure) and the [Azure workload identity documentation](https://azure.github.io/azure-workload-identity/docs/) for more details. + +#### Workload Identity UI configuration + +| Setting | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------------- | +| **Authentication** | Select **Workload Identity**. The directory ID, application ID, and client secret fields are hidden. | +| **Default Subscription** | Click **Load Subscriptions** to populate available subscriptions, then select your default. | + +{{< figure src="/media/docs/grafana/data-sources/screenshot-workload-identity.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor data source configured with Workload Identity" >}} + +#### Provision Workload Identity with YAML + +```yaml +apiVersion: 1 + +datasources: + - name: Azure Monitor + type: grafana-azure-monitor-datasource + access: proxy + jsonData: + azureAuthType: workloadidentity + subscriptionId: # Optional, default subscription + version: 1 +``` + +### Current User + +Forward the logged-in Grafana user's Azure credentials to the data source for user-level access control. + +{{< admonition type="warning" >}} +Current User authentication is an [experimental feature](/docs/release-life-cycle/). Engineering and on-call support is not available. Documentation is limited. No SLA is provided. Contact Grafana Support to enable this feature in Grafana Cloud. +{{< /admonition >}} + +#### Current User prerequisites + +Your Grafana instance must be configured with Microsoft Entra ID authentication. Refer to the [Microsoft Entra ID authentication documentation](ref:configure-grafana-azure-auth). + +#### Configure your Azure App Registration + +The App Registration used for Grafana login requires additional configuration: + +**Enable token issuance:** + +1. In the Azure Portal, open your App Registration. +1. Select **Authentication** in the side menu. +1. Under **Implicit grant and hybrid flows**, check both **Access tokens** and **ID tokens**. +1. Save your changes. + +**Add API permissions:** + +1. In the Azure Portal, open your App Registration. +1. Select **API Permissions** in the side menu. +1. Ensure these permissions are present under **Microsoft Graph**: `openid`, `profile`, `email`, and `offline_access`. +1. Add the following permissions: + - **Azure Service Management** > **Delegated permissions** > `user_impersonation` + - **APIs my organization uses** > Search for **Log Analytics API** > **Delegated permissions** > `Data.Read` + +Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information. + +**Update Grafana scopes:** + +Update the `scopes` section in your Grafana Azure authentication configuration to include the `.default` scope: + +``` +.default openid email profile +``` + +#### Current User Grafana server configuration + +Enable current user authentication in the Grafana server configuration: + +```ini +[azure] +user_identity_enabled = true +``` + +By default, this also enables fallback service credentials. To disable fallback credentials at the instance level: + +```ini +[azure] +user_identity_enabled = true +user_identity_fallback_credentials_enabled = false +``` + +{{< admonition type="note" >}} +To use fallback service credentials, the [feature toggle](ref:configure-grafana-feature-toggles) `idForwarding` must be set to `true`. +{{< /admonition >}} + +#### Limitations and fallback credentials + +Current User authentication doesn't support backend functionality like alerting, reporting, and recorded queries because user credentials aren't available for background operations. + +To support these features, configure **fallback service credentials**. When enabled, Grafana uses the fallback credentials for backend operations. Note that operations using fallback credentials are limited to the permissions of those credentials, not the user's permissions. + +{{< admonition type="note" >}} +Query and resource caching is disabled by default for data sources using Current User authentication. +{{< /admonition >}} + +#### Current User UI configuration + +| Setting | Description | +| -------------------------------- | ------------------------------------------------------------------------------------------- | +| **Authentication** | Select **Current User**. | +| **Default Subscription** | Click **Load Subscriptions** to populate available subscriptions, then select your default. | +| **Fallback Service Credentials** | Enable and configure credentials for backend features like alerting. | + +{{< figure src="/media/docs/grafana/data-sources/screenshot-current-user.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor data source configured with Current User authentication" >}} + +#### Provision Current User with YAML + +{{< admonition type="note" >}} +The `oauthPassThru` property is required for Current User authentication. The `disableGrafanaCache` property prevents returning cached responses for resources users don't have access to. +{{< /admonition >}} + +```yaml +apiVersion: 1 + +datasources: + - name: Azure Monitor + type: grafana-azure-monitor-datasource + access: proxy + jsonData: + azureAuthType: currentuser + oauthPassThru: true + disableGrafanaCache: true + subscriptionId: # Optional, default subscription + version: 1 +``` + +## Additional configuration options + +These settings apply to all authentication methods. + +### General settings + +| Setting | Description | +| ----------- | ------------------------------------------------------------------------------- | +| **Name** | The data source name used in panels and queries. Example: `azure-monitor-prod`. | +| **Default** | Toggle to make this the default data source for new panels. | + +### Enable Basic Logs + +Toggle **Enable Basic Logs** to allow queries against [Basic Logs tables](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/basic-logs-query?tabs=portal-1) in supported Log Analytics Workspaces. + +{{< admonition type="note" >}} +Querying Basic Logs tables incurs additional costs on a per-query basis. +{{< /admonition >}} + +### Private data source connect (Grafana Cloud only) + +If you're using Grafana Cloud and need to connect to Azure resources in a private network, use Private Data Source Connect (PDC). + +1. Click the **Private data source connect** dropdown to select your PDC configuration. +1. Click **Manage private data source connect** to view your PDC connection details. + +For more information, refer to [Private data source connect](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). + +## Supported cloud names + +When provisioning the data source, use the following `cloudName` values: + +| Azure Cloud | `cloudName` value | +| -------------------------------- | ------------------------ | +| Microsoft Azure public cloud | `azuremonitor` (default) | +| Microsoft Chinese national cloud | `chinaazuremonitor` | +| US Government cloud | `govazuremonitor` | + +{{< admonition type="note" >}} +For Current User authentication, the cloud names differ: use `AzureCloud` for public cloud, `AzureChinaCloud` for the Chinese national cloud, and `AzureUSGovernment` for the US Government cloud. +{{< /admonition >}} + +## Verify the connection + +After configuring the data source, click **Save & test**. A successful connection displays a message confirming that the credentials are valid and have access to the configured default subscription. + +If the test fails, verify: + +- Your credentials are correct (tenant ID, client ID, client secret) +- The identity has the required Azure RBAC permissions +- For Managed Identity or Workload Identity, that the Grafana server configuration is correct +- Network connectivity to Azure endpoints + +## Provision the data source + +You can define and configure the Azure Monitor data source in YAML files as part of the Grafana provisioning system. +For more information about provisioning, refer to [Provisioning Grafana](ref:provisioning-data-sources). + +### Provision quick reference + +| Authentication method | `azureAuthType` value | Required fields | +| --------------------- | --------------------- | -------------------------------------------------- | +| App Registration | `clientsecret` | `tenantId`, `clientId`, `clientSecret` | +| Managed Identity | `msi` | None (uses VM identity) | +| Workload Identity | `workloadidentity` | None (uses pod identity) | +| Current User | `currentuser` | `oauthPassThru: true`, `disableGrafanaCache: true` | + +All methods support the optional `subscriptionId` field to set a default subscription. + +For complete YAML examples, see the [authentication method sections](#configure-authentication) above. + +## Configure with Terraform + +You can configure the Azure Monitor data source using the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). This approach enables infrastructure-as-code workflows and version control for your Grafana configuration. + +### Terraform prerequisites + +- [Terraform](https://www.terraform.io/downloads) installed +- Grafana Terraform provider configured with appropriate credentials +- For Grafana Cloud: A [Cloud Access Policy token](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) with data source permissions + +### Provider configuration + +Configure the Grafana provider to connect to your Grafana instance: + +```hcl +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = ">= 2.0.0" + } + } +} + +# For Grafana Cloud +provider "grafana" { + url = "" + auth = "" +} + +# For self-hosted Grafana +# provider "grafana" { +# url = "http://localhost:3000" +# auth = "" +# } +``` + +### Terraform examples + +The following examples show how to configure the Azure Monitor data source for each authentication method. + +**App Registration (client secret):** + +```hcl +resource "grafana_data_source" "azure_monitor" { + type = "grafana-azure-monitor-datasource" + name = "Azure Monitor" + + json_data_encoded = jsonencode({ + azureAuthType = "clientsecret" + cloudName = "azuremonitor" + tenantId = "" + clientId = "" + subscriptionId = "" + }) + + secure_json_data_encoded = jsonencode({ + clientSecret = "" + }) +} +``` + +**Managed Identity:** + +```hcl +resource "grafana_data_source" "azure_monitor" { + type = "grafana-azure-monitor-datasource" + name = "Azure Monitor" + + json_data_encoded = jsonencode({ + azureAuthType = "msi" + subscriptionId = "" + }) +} +``` + +**Workload Identity:** + +```hcl +resource "grafana_data_source" "azure_monitor" { + type = "grafana-azure-monitor-datasource" + name = "Azure Monitor" + + json_data_encoded = jsonencode({ + azureAuthType = "workloadidentity" + subscriptionId = "" + }) +} +``` + +**Current User:** + +```hcl +resource "grafana_data_source" "azure_monitor" { + type = "grafana-azure-monitor-datasource" + name = "Azure Monitor" + + json_data_encoded = jsonencode({ + azureAuthType = "currentuser" + oauthPassThru = true + disableGrafanaCache = true + subscriptionId = "" + }) +} +``` + +**With Basic Logs enabled:** + +Add `enableBasicLogs = true` to any of the above configurations: + +```hcl +resource "grafana_data_source" "azure_monitor" { + type = "grafana-azure-monitor-datasource" + name = "Azure Monitor" + + json_data_encoded = jsonencode({ + azureAuthType = "clientsecret" + cloudName = "azuremonitor" + tenantId = "" + clientId = "" + subscriptionId = "" + enableBasicLogs = true + }) + + secure_json_data_encoded = jsonencode({ + clientSecret = "" + }) +} +``` + +For more information about the Grafana Terraform provider, refer to the [provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs) and the [grafana_data_source resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source). diff --git a/docs/sources/datasources/azure-monitor/query-editor/index.md b/docs/sources/datasources/azure-monitor/query-editor/index.md index 6415be1281c..a8c763d9280 100644 --- a/docs/sources/datasources/azure-monitor/query-editor/index.md +++ b/docs/sources/datasources/azure-monitor/query-editor/index.md @@ -21,6 +21,7 @@ labels: menuTitle: Query editor title: Azure Monitor query editor weight: 300 +last_reviewed: 2025-12-04 refs: query-transform-data-query-options: - pattern: /docs/grafana/ @@ -32,30 +33,85 @@ refs: destination: /docs/grafana//panels-visualizations/query-transform-data/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//panels-visualizations/query-transform-data/ + configure-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + explore: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//explore/ + troubleshoot-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/troubleshooting/ + configure-grafana-feature-toggles: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/feature-toggles/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/feature-toggles/ + template-variables: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + alerting-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/alerting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/alerting/ + annotations-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/annotations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/annotations/ --- # Azure Monitor query editor -This topic explains querying specific to the Azure Monitor data source. -For general documentation on querying data sources in Grafana, see [Query and transform data](ref:query-transform-data). +Grafana provides a query editor for the Azure Monitor data source, which is located on the [Explore page](ref:explore). You can also access the Azure Monitor query editor from a dashboard panel. Click the menu in the upper right of the panel and select **Edit**. -## Choose a query editing mode +This document explains querying specific to the Azure Monitor data source. +For general documentation on querying data sources in Grafana, refer to [Query and transform data](ref:query-transform-data). -The Azure Monitor data source's query editor has three modes depending on which Azure service you want to query: +The Azure Monitor data source can query data from Azure Monitor Metrics and Logs, the Azure Resource Graph, and Application Insights Traces. Each source has its own specialized query editor. + +## Before you begin + +- Ensure you have [configured the Azure Monitor data source](ref:configure-azure-monitor). +- Verify your credentials have appropriate permissions for the resources you want to query. + +## Key concepts + +If you're new to Azure Monitor, here are some key terms used throughout this documentation: + +| Term | Description | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **KQL (Kusto Query Language)** | The query language used for Azure Monitor Logs and Azure Resource Graph. KQL uses a pipe-based syntax similar to Unix commands and is optimized for read-only data exploration. If you know SQL, the [SQL to Kusto cheat sheet](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/sqlcheatsheet) can help you get started. | +| **Log Analytics workspace** | An Azure resource that collects and stores log data from your Azure resources, applications, and services. You query this data using KQL. | +| **Application Insights** | Azure's application performance monitoring (APM) service. It collects telemetry data like requests, exceptions, and traces from your applications. | +| **Metrics vs. Logs** | **Metrics** are lightweight numeric values collected at regular intervals (e.g., CPU percentage). **Logs** are detailed records of events with varying schemas (e.g., request logs, error messages). Metrics use a visual query builder; Logs require KQL. | + +## Choose a query editor mode + +The Azure Monitor data source's query editor has four modes depending on which Azure service you want to query: - **Metrics** for [Azure Monitor Metrics](#query-azure-monitor-metrics) - **Logs** for [Azure Monitor Logs](#query-azure-monitor-logs) -- [**Azure Resource Graph**](#query-azure-resource-graph) - **Traces** for [Application Insights Traces](#query-application-insights-traces) +- **Azure Resource Graph** for [Azure Resource Graph](#query-azure-resource-graph) ## Query Azure Monitor Metrics -Azure Monitor Metrics collects numeric data from [supported resources](https://docs.microsoft.com/en-us/azure/azure-monitor/monitor-reference), and you can query them to investigate your resources' health and usage and maximise availability and performance. +Azure Monitor Metrics collects numeric data from [supported resources](https://docs.microsoft.com/en-us/azure/azure-monitor/monitor-reference), and you can query them to investigate your resources' health and usage and maximize availability and performance. Monitor Metrics use a lightweight format that stores only numeric data in a specific structure and supports near real-time scenarios, making it useful for fast detection of issues. In contrast, Azure Monitor Logs can store a variety of data types, each with their own structure. -{{< figure src="/static/img/docs/azure-monitor/query-editor-metrics.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Logs Metrics sample query visualizing CPU percentage over time" >}} +{{< figure src="/static/img/docs/azure-monitor/query-editor-metrics.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor Metrics sample query visualizing CPU percentage over time" >}} ### Create a Metrics query @@ -85,7 +141,7 @@ Optionally, you can apply further aggregations or filter by dimensions. The available options change depending on what is relevant to the selected metric. -You can also augment queries by using [template variables](../template-variables/). +You can also augment queries by using [template variables](ref:template-variables). ### Format legend aliases @@ -109,7 +165,7 @@ For example: | `{{ dimensionname }}` | _(Legacy for backward compatibility)_ Replaced with the name of the first dimension. | | `{{ dimensionvalue }}` | _(Legacy for backward compatibility)_ Replaced with the value of the first dimension. | -### Filter using dimensions +### Filter with dimensions Some metrics also have dimensions, which associate additional metadata. Dimensions are represented as key-value pairs assigned to each value of a metric. @@ -121,7 +177,7 @@ For more information on multi-dimensional metrics, refer to the [Azure Monitor d ## Query Azure Monitor Logs -Azure Monitor Logs collects and organises log and performance data from [supported resources](https://docs.microsoft.com/en-us/azure/azure-monitor/monitor-reference), and makes many sources of data available to query together with the [Kusto Query Language (KQL)](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/). +Azure Monitor Logs collects and organizes log and performance data from [supported resources](https://docs.microsoft.com/en-us/azure/azure-monitor/monitor-reference), and makes many sources of data available to query together with the [Kusto Query Language (KQL)](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/). While Azure Monitor Metrics stores only simplified numerical data, Logs can store different data types, each with their own structure. You can also perform complex analysis of Logs data by using KQL. @@ -130,6 +186,32 @@ The Azure Monitor data source also supports querying of [Basic Logs](https://lea {{< figure src="/static/img/docs/azure-monitor/query-editor-logs.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor Logs sample query comparing successful requests to failed requests" >}} +### Logs query builder (public preview) + +{{< admonition type="note" >}} +The Logs query builder is a [public preview feature](/docs/release-life-cycle/). It may not be enabled in all Grafana environments. +{{< /admonition >}} + +The Logs query builder provides a visual interface for building Azure Monitor Logs queries without writing KQL. This is helpful if you're new to KQL or want to quickly build simple queries. + +**To enable the Logs query builder:** + +1. Enable the `azureMonitorLogsBuilderEditor` [feature toggle](ref:configure-grafana-feature-toggles) in your Grafana configuration. +1. Restart Grafana for the change to take effect. + +**To switch between Builder and Code modes:** + +When the feature is enabled, a **Builder / Code** toggle appears in the Logs query editor: + +- **Builder**: Use the visual interface to select tables, columns, filters, and aggregations. The builder generates the KQL query for you. +- **Code**: Write KQL queries directly. Use this mode for complex queries that require full KQL capabilities. + +New queries default to Builder mode. Existing queries that were created with raw KQL remain in Code mode. + +{{< admonition type="note" >}} +You can switch from Builder to Code mode at any time to view or edit the generated KQL. However, switching from Code to Builder mode may not preserve complex queries that can't be represented in the builder interface. +{{< /admonition >}} + ### Create a Logs query **To create a Logs query:** @@ -140,13 +222,13 @@ The Azure Monitor data source also supports querying of [Basic Logs](https://lea Alternatively, you can dynamically query all resources under a single resource group or subscription. {{< admonition type="note" >}} - If a timespan is specified in the query, the overlap of the timespan between the query and the dashboard will be used as the query timespan. See the [API documentation for + If a time span is specified in the query, the overlap between the query time span and the dashboard time range will be used. See the [API documentation for details.](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters) {{< /admonition >}} 1. Enter your KQL query. -You can also augment queries by using [template variables](../template-variables/). +You can also augment queries by using [template variables](ref:template-variables). **To create a Basic Logs query:** @@ -161,7 +243,7 @@ You can also augment queries by using [template variables](../template-variables {{< /admonition >}} 1. Enter your KQL query. -You can also augment queries by using [template variables](https://grafana.com/docs/grafana//datasources/azure-monitor/template-variables/). +You can also augment queries by using [template variables](ref:template-variables). ### Logs query examples @@ -174,24 +256,28 @@ The Azure documentation includes resources to help you learn KQL: - [Tutorial: Use Kusto queries in Azure Monitor](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/tutorial?pivots=azuremonitor) - [SQL to Kusto cheat sheet](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/sqlcheatsheet) -> **Time-range:** The time-range that will be used for the query can be modified via the time-range switch. Selecting `Query` will only make use of time-ranges specified within the query. -> Specifying `Dashboard` will only make use of the Grafana time-range. -> If there are no time-ranges specified within the query, the default Log Analytics time-range will apply. -> For more details on this change, refer to the [Azure Monitor Logs API documentation](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters). -> If the `Intersection` option was previously chosen it will be migrated by default to `Dashboard`. +{{< admonition type="note" >}} +**Time-range:** The time-range used for the query can be modified via the time-range switch: -This example query returns a virtual machine's CPU performance, averaged over 5ms time grains: +- Selecting **Query** uses only time-ranges specified within the query. +- Selecting **Dashboard** uses only the Grafana dashboard time-range. +- If no time-range is specified in the query, the default Log Analytics time-range applies. + +For more details, refer to the [Azure Monitor Logs API documentation](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters). If you previously used the `Intersection` option, it has been migrated to `Dashboard`. +{{< /admonition >}} + +This example query returns a virtual machine's CPU performance, averaged over 5-minute time grains: ```kusto Perf -# $__timeFilter is a special Grafana macro that filters the results to the time span of the dashboard +// $__timeFilter is a special Grafana macro that filters the results to the time span of the dashboard | where $__timeFilter(TimeGenerated) | where CounterName == "% Processor Time" | summarize avg(CounterValue) by bin(TimeGenerated, 5m), Computer | order by TimeGenerated asc ``` -Use time series queries for values that change over time, usually for graph visualisations such as the Time series panel. +Use time series queries for values that change over time, usually for graph visualizations such as the Time series panel. Each query should return at least a datetime column and numeric value column. The result must also be sorted in ascending order by the datetime column. @@ -357,21 +443,33 @@ Application Insights stores trace data in an underlying Log Analytics workspace This query type only supports Application Insights resources. {{< /admonition >}} -Running a query of this kind will return all trace data within the timespan specified by the panel/dashboard. +1. (Optional) Specify an **Operation ID** value to filter traces. +1. (Optional) Specify **event types** to filter by. +1. (Optional) Specify **event properties** to filter by. +1. (Optional) Change the **Result format** to switch between tabular format and trace format. -Optionally, you can apply further filtering or select a specific Operation ID to query. The result format can also be switched between a tabular format or the trace format which will return the data in a format that can be used with the Trace visualization. + {{< admonition type="note" >}} + Selecting the trace format filters events to only the `trace` type. Use this format with the Trace visualization. + {{< /admonition >}} -{{< admonition type="note" >}} -Selecting the trace format will filter events with the `trace` type. -{{< /admonition >}} +Running a query returns all trace data within the time span specified by the panel or dashboard time range. -1. Specify an Operation ID value. -1. Specify event types to filter by. -1. Specify event properties to filter by. +You can also augment queries by using [template variables](ref:template-variables). -You can also augment queries by using [template variables](../template-variables/). +## Use queries for alerting and recording rules -## Working with large Azure resource data sets +All Azure Monitor query types (Metrics, Logs, Azure Resource Graph, and Traces) can be used with Grafana Alerting and recording rules. + +For detailed information about creating alert rules, supported query types, authentication requirements, and examples, refer to [Azure Monitor alerting](ref:alerting-azure-monitor). + +## Work with large Azure resource datasets If a request exceeds the [maximum allowed value of records](https://docs.microsoft.com/en-us/azure/governance/resource-graph/concepts/work-with-data#paging-results), the result is paginated and only the first page of results are returned. You can use filters to reduce the amount of records returned under that value. + +## Next steps + +- [Use template variables](../template-variables/) to create dynamic, reusable dashboards +- [Add annotations](ref:annotations-azure-monitor) to overlay events on your graphs +- [Set up alerting](ref:alerting-azure-monitor) to create alert rules based on Azure Monitor data +- [Troubleshoot](ref:troubleshoot-azure-monitor) common query and configuration issues diff --git a/docs/sources/datasources/azure-monitor/template-variables/index.md b/docs/sources/datasources/azure-monitor/template-variables/index.md index 1db472a4251..3cedadef9b5 100644 --- a/docs/sources/datasources/azure-monitor/template-variables/index.md +++ b/docs/sources/datasources/azure-monitor/template-variables/index.md @@ -23,6 +23,7 @@ labels: menuTitle: Template variables title: Azure Monitor template variables weight: 400 +last_reviewed: 2025-12-04 refs: variables: - pattern: /docs/grafana/ @@ -34,6 +35,11 @@ refs: destination: /docs/grafana//dashboards/variables/add-template-variables/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//dashboards/variables/add-template-variables/ + configure-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/configure/ --- # Azure Monitor template variables @@ -42,58 +48,173 @@ Instead of hard-coding details such as resource group or resource name values in This helps you create more interactive, dynamic, and reusable dashboards. Grafana refers to such variables as template variables. -For an introduction to templating and template variables, refer to the [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables) documentation. +For an introduction to templating and template variables, refer to the [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables). -## Use query variables +## Before you begin -You can specify these Azure Monitor data source queries in the Variable edit view's **Query Type** field. +- Ensure you have [configured the Azure Monitor data source](ref:configure-azure-monitor). +- If you want template variables to auto-populate subscriptions, set a **Default Subscription** in the data source configuration. -| Name | Description | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| **Subscriptions** | Returns subscriptions. | -| **Resource Groups** | Returns resource groups for a specified subscription. Supports multi-value. | -| **Namespaces** | Returns metric namespaces for the specified subscription. If a resource group is provided, only the namespaces within that group are returned. | -| **Regions** | Returns regions for the specified subscription | -| **Resource Names** | Returns a list of resource names for a specified subscription, resource group and namespace. Supports multi-value. | -| **Metric Names** | Returns a list of metric names for a resource. | -| **Workspaces** | Returns a list of workspaces for the specified subscription. | -| **Logs** | Use a KQL query to return values. | -| **Custom Namespaces** | Returns metric namespaces for the specified resource. | -| **Custom Metric Names** | Returns a list of custom metric names for the specified resource. | +## Create a template variable + +To create a template variable for Azure Monitor: + +1. Open the dashboard where you want to add the variable. +1. Click **Dashboard settings** (gear icon) in the top navigation. +1. Select **Variables** in the left menu. +1. Click **Add variable**. +1. Enter a **Name** for your variable (e.g., `subscription`, `resourceGroup`, `resource`). +1. In the **Type** dropdown, select **Query**. +1. In the **Data source** dropdown, select your Azure Monitor data source. +1. In the **Query Type** dropdown, select the appropriate query type (see [Available query types](#available-query-types)). +1. Configure any additional fields required by the selected query type. +1. Click **Run query** to preview the variable values. +1. Configure display options such as **Multi-value** or **Include All option** as needed. +1. Click **Apply** to save the variable. + +## Available query types + +The Azure Monitor data source provides the following query types for template variables: + +| Query type | Description | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| **Subscriptions** | Returns a list of Azure subscriptions accessible to the configured credentials. | +| **Resource Groups** | Returns resource groups for a specified subscription. Supports multi-value selection. | +| **Namespaces** | Returns metric namespaces for the specified subscription. If a resource group is specified, returns only namespaces within that group. | +| **Regions** | Returns Azure regions available for the specified subscription. | +| **Resource Names** | Returns resource names for a specified subscription, resource group, and namespace. Supports multi-value selection. | +| **Metric Names** | Returns available metric names for a specified resource. | +| **Workspaces** | Returns Log Analytics workspaces for the specified subscription. | +| **Logs** | Executes a KQL query and returns the results as variable values. See [Create a Logs variable](#create-a-logs-variable). | +| **Custom Namespaces** | Returns custom metric namespaces for a specified resource. | +| **Custom Metric Names** | Returns custom metric names for a specified resource. | {{< admonition type="note" >}} -Custom metrics cannot be emitted against a subscription or resource group. Select resources only when you need to retrieve custom metric namespaces or custom metric names associated with a specific resource. +Custom metrics cannot be emitted against a subscription or resource group. Select specific resources when retrieving custom metric namespaces or custom metric names. {{< /admonition >}} -You can use any Log Analytics Kusto Query Language (KQL) query that returns a single list of values in the `Query` field. -For example: +## Create cascading variables -| Query | List of values returned | -| ----------------------------------------------------------------------------------------- | --------------------------------------- | -| `workspace("myWorkspace").Heartbeat \| distinct Computer` | Virtual machines | -| `workspace("$workspace").Heartbeat \| distinct Computer` | Virtual machines with template variable | -| `workspace("$workspace").Perf \| distinct ObjectName` | Objects from the Perf table | -| `workspace("$workspace").Perf \| where ObjectName == "$object"` `\| distinct CounterName` | Metric names from the Perf table | +Cascading variables (also called dependent or chained variables) allow you to create dropdown menus that filter based on previous selections. This is useful for drilling down from subscription to resource group to specific resource. -### Query variable example +### Example: Subscription → Resource Group → Resource Name -This time series query uses query variables: +**Step 1: Create a Subscription variable** + +1. Create a variable named `subscription`. +1. Set **Query Type** to **Subscriptions**. + +**Step 2: Create a Resource Group variable** + +1. Create a variable named `resourceGroup`. +1. Set **Query Type** to **Resource Groups**. +1. In the **Subscription** field, select `$subscription`. + +**Step 3: Create a Resource Name variable** + +1. Create a variable named `resource`. +1. Set **Query Type** to **Resource Names**. +1. In the **Subscription** field, select `$subscription`. +1. In the **Resource Group** field, select `$resourceGroup`. +1. Select the appropriate **Namespace** for your resources (e.g., `Microsoft.Compute/virtualMachines`). + +Now when you change the subscription, the resource group dropdown updates automatically, and when you change the resource group, the resource name dropdown updates. + +## Create a Logs variable + +The **Logs** query type lets you use a KQL query to populate variable values. The query must return a single column of values. + +**To create a Logs variable:** + +1. Create a new variable with **Query Type** set to **Logs**. +1. Select a **Resource** (Log Analytics workspace or Application Insights resource). +1. Enter a KQL query that returns a single column. + +### Logs variable query examples + +| Query | Returns | +| ----------------------------------------- | ------------------------------------- | +| `Heartbeat \| distinct Computer` | List of virtual machine names | +| `Perf \| distinct ObjectName` | List of performance object names | +| `AzureActivity \| distinct ResourceGroup` | List of resource groups with activity | +| `AppRequests \| distinct Name` | List of application request names | + +You can reference other variables in your Logs query: + +```kusto +workspace("$workspace").Heartbeat | distinct Computer +``` + +```kusto +workspace("$workspace").Perf +| where ObjectName == "$object" +| distinct CounterName +``` + +## Variable refresh options + +Control when your variables refresh by setting the **Refresh** option: + +| Option | Behavior | +| ------------------------ | ----------------------------------------------------------------------------------------- | +| **On dashboard load** | Variables refresh each time the dashboard loads. Best for data that changes infrequently. | +| **On time range change** | Variables refresh when the dashboard time range changes. Use for time-sensitive queries. | + +For dashboards with many variables or complex queries, use **On dashboard load** to improve performance. + +## Use variables in queries + +After you create template variables, you can use them in your Azure Monitor queries by referencing them with the `$` prefix. + +### Metrics query example + +In a Metrics query, select your variables in the resource picker fields: + +- **Subscription**: `$subscription` +- **Resource Group**: `$resourceGroup` +- **Resource Name**: `$resource` + +### Logs query example + +Reference variables directly in your KQL queries: ```kusto Perf | where ObjectName == "$object" and CounterName == "$metric" | where TimeGenerated >= $__timeFrom() and TimeGenerated <= $__timeTo() -| where $__contains(Computer, $computer) +| where $__contains(Computer, $computer) | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer | order by TimeGenerated asc ``` -### Multi-value variables +## Multi-value variables -It is possible to select multiple values for **Resource Groups** and **Resource Names** and use a single metrics query pointing to those values as long as they: +You can enable **Multi-value** selection for **Resource Groups** and **Resource Names** variables. When using multi-value variables in a Metrics query, all selected resources must: -- Belong to the same subscription. -- Are in the same region. -- Are of the same type (namespace). +- Belong to the same subscription +- Be in the same Azure region +- Be of the same resource type (namespace) -Also, note that if a template variable pointing to multiple resource groups or names is used in another template variable as a parameter (e.g. to retrieve metric names), only the first value will be used. This means that the combination of the first resource group and name selected should be valid. +{{< admonition type="note" >}} +When a multi-value variable is used as a parameter in another variable query (for example, to retrieve metric names), only the first selected value is used. Ensure the first resource group and resource name combination is valid. +{{< /admonition >}} + +## Troubleshoot template variables + +If you encounter issues with template variables, try the following solutions. + +### Variable returns no values + +- Verify the Azure Monitor data source is configured correctly and can connect to Azure. +- Check that the credentials have appropriate permissions to list the requested resources. +- For cascading variables, ensure parent variables have valid selections. + +### Variable values are outdated + +- Check the **Refresh** setting and adjust if needed. +- Click the refresh icon next to the variable dropdown to manually refresh. + +### Multi-value selection not working in queries + +- Ensure the resources meet the requirements (same subscription, region, and type). +- For Logs queries, use the `$__contains()` macro to handle multi-value variables properly. diff --git a/docs/sources/datasources/azure-monitor/troubleshooting/index.md b/docs/sources/datasources/azure-monitor/troubleshooting/index.md new file mode 100644 index 00000000000..b2d5a9efc32 --- /dev/null +++ b/docs/sources/datasources/azure-monitor/troubleshooting/index.md @@ -0,0 +1,320 @@ +--- +aliases: + - ../../data-sources/azure-monitor/troubleshooting/ +description: Troubleshooting guide for the Azure Monitor data source in Grafana +keywords: + - grafana + - azure + - monitor + - troubleshooting + - errors + - authentication + - query +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Troubleshoot +title: Troubleshoot Azure Monitor data source issues +weight: 500 +last_reviewed: 2025-12-04 +refs: + configure-azure-monitor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/configure/ + template-variables: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/template-variables/ + query-editor: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//datasources/azure-monitor/query-editor/ +--- + +# Troubleshoot Azure Monitor data source issues + +This document provides solutions to common issues you may encounter when configuring or using the Azure Monitor data source. + +## Configuration and authentication errors + +These errors typically occur when setting up the data source or when authentication credentials are invalid. + +### "Authorization failed" or "Access denied" + +**Symptoms:** + +- Save & test fails with "Authorization failed" +- Queries return "Access denied" errors +- Subscriptions don't load when clicking **Load Subscriptions** + +**Possible causes and solutions:** + +| Cause | Solution | +| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| App registration doesn't have required permissions | Assign the `Reader` role to the app registration on the subscription or resource group you want to monitor. Refer to the [Azure documentation for role assignments](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current). | +| Incorrect tenant ID, client ID, or client secret | Verify the credentials in the Azure Portal under **App registrations** > your app > **Overview** (for IDs) and **Certificates & secrets** (for secret). | +| Client secret has expired | Create a new client secret in Azure and update the data source configuration. | +| Managed Identity not enabled on the Azure resource | For VMs, enable managed identity in the Azure Portal under **Identity**. For App Service, enable it under **Identity** in the app settings. | +| Managed Identity not assigned the Reader role | Assign the `Reader` role to the managed identity on the target subscription or resources. | + +### "Invalid client secret" or "Client secret not found" + +**Symptoms:** + +- Authentication fails immediately after configuration +- Error message references invalid credentials + +**Solutions:** + +1. Ensure you copied the client secret **value**, not the secret ID. In Azure Portal under **Certificates & secrets**, the secret value is only shown once when created. The secret ID is a different identifier and won't work for authentication. +2. Verify the client secret was copied correctly (no extra spaces or truncation). +3. Check if the secret has expired in Azure Portal under **App registrations** > your app > **Certificates & secrets**. +4. Create a new secret and update the data source configuration. + +### "Tenant not found" or "Invalid tenant ID" + +**Symptoms:** + +- Data source test fails with tenant-related errors +- Unable to authenticate + +**Solutions:** + +1. Verify the Directory (tenant) ID in Azure Portal under **Microsoft Entra ID** > **Overview**. +2. Ensure you're using the correct Azure cloud setting (Azure, Azure Government, or Azure China). +3. Check that the tenant ID is a valid GUID format. + +### Managed Identity not working + +**Symptoms:** + +- Managed Identity option is available but authentication fails +- Error: "Managed identity authentication is not available" + +**Solutions:** + +1. Verify `managed_identity_enabled = true` is set in the Grafana server configuration under `[azure]`. +2. Confirm the Azure resource hosting Grafana has managed identity enabled. +3. For user-assigned managed identity, ensure `managed_identity_client_id` is set correctly. +4. Verify the managed identity has the `Reader` role on the target resources. +5. Restart Grafana after changing server configuration. + +### Workload Identity not working + +**Symptoms:** + +- Workload Identity authentication fails in Kubernetes/AKS environment +- Token file errors + +**Solutions:** + +1. Verify `workload_identity_enabled = true` is set in the Grafana server configuration. +2. Check that the service account is correctly annotated for workload identity. +3. Verify the federated credential is configured in Azure. +4. Ensure the token path is accessible to the Grafana pod. +5. Check the workload identity webhook is running in the cluster. + +## Query errors + +These errors occur when executing queries against Azure Monitor services. + +### "No data" or empty results + +**Symptoms:** + +- Query executes without error but returns no data +- Charts show "No data" message + +**Possible causes and solutions:** + +| Cause | Solution | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Time range doesn't contain data | Expand the dashboard time range or verify data exists in Azure Portal. | +| Wrong resource selected | Verify you've selected the correct subscription, resource group, and resource. | +| Metric not available for resource | Not all metrics are available for all resources. Check available metrics in Azure Portal under the resource's **Metrics** blade. | +| Metric has no values | Some metrics only populate under certain conditions (e.g., error counts when errors occur). | +| Permissions issue | Verify the identity has read access to the specific resource. | + +### "Bad request" or "Invalid query" + +**Symptoms:** + +- Query fails with 400 error +- Error message indicates query syntax issues + +**Solutions for Logs queries:** + +1. Validate your KQL syntax in the Azure Portal Log Analytics query editor. +2. Check for typos in table names or column names. +3. Ensure referenced tables exist in the selected workspace. +4. Verify the time range is valid (not in the future, not too far in the past for data retention). + +**Solutions for Metrics queries:** + +1. Verify the metric name is valid for the selected resource type. +2. Check that dimension filters use valid dimension names and values. +3. Ensure the aggregation type is supported for the selected metric. + +### "Resource not found" + +**Symptoms:** + +- Query fails with 404 error +- Resource picker shows resources that can't be queried + +**Solutions:** + +1. Verify the resource still exists in Azure (it may have been deleted or moved). +2. Check that the subscription is correct. +3. Refresh the resource picker by re-selecting the subscription. +4. Verify the identity has access to the resource's resource group. + +### Logs query timeout + +**Symptoms:** + +- Query runs for a long time then fails +- Error mentions timeout or query limits + +**Solutions:** + +1. Narrow the time range to reduce data volume. +2. Add filters to reduce the result set. +3. Use `summarize` to aggregate data instead of returning raw rows. +4. Consider using Basic Logs for large datasets (if enabled). +5. Break complex queries into smaller parts. + +### "Metrics not available" for a resource + +**Symptoms:** + +- Resource appears in picker but no metrics are listed +- Metric dropdown is empty + +**Solutions:** + +1. Verify the resource type supports Azure Monitor metrics. +2. Check if the resource is in a region that supports metrics. +3. Some resources require diagnostic settings to emit metrics—configure these in Azure Portal. +4. Try selecting a different namespace for the resource. + +## Azure Resource Graph errors + +These errors are specific to Azure Resource Graph (ARG) queries. + +### "Query execution failed" + +**Symptoms:** + +- ARG query fails with execution errors +- Results don't match expected resources + +**Solutions:** + +1. Validate query syntax in Azure Portal Resource Graph Explorer. +2. Check that you have access to the subscriptions being queried. +3. Verify table names are correct (e.g., `Resources`, `ResourceContainers`). +4. Some ARG features require specific permissions, check [ARG documentation](https://docs.microsoft.com/en-us/azure/governance/resource-graph/). + +### Query returns incomplete results + +**Symptoms:** + +- Not all expected resources appear in results +- Results seem truncated + +**Solutions:** + +1. ARG queries are paginated. The data source handles pagination automatically, but very large result sets may be limited. +2. Add filters to reduce result set size. +3. Verify you have access to all subscriptions containing the resources. + +## Application Insights Traces errors + +These errors are specific to the Traces query type. + +### "No traces found" + +**Symptoms:** + +- Trace query returns empty results +- Operation ID search finds nothing + +**Solutions:** + +1. Verify the Application Insights resource is collecting trace data. +2. Check that the time range includes when the traces were generated. +3. Ensure the Operation ID is correct (copy directly from another trace or log). +4. Verify the identity has access to the Application Insights resource. + +## Template variable errors + +For detailed troubleshooting of template variables, refer to the [template variables troubleshooting section](ref:template-variables). + +### Variables return no values + +**Solutions:** + +1. Verify the data source connection is working (test it in the data source settings). +2. Check that parent variables (for cascading variables) have valid selections. +3. Verify the identity has permissions to list the requested resources. +4. For Logs variables, ensure the KQL query returns a single column. + +### Variables are slow to load + +**Solutions:** + +1. Set variable refresh to **On dashboard load** instead of **On time range change**. +2. Reduce the scope of variable queries (e.g., filter by resource group instead of entire subscription). +3. For Logs variables, optimize the KQL query to return results faster. + +## Connection and network errors + +These errors indicate problems with network connectivity between Grafana and Azure services. + +### "Connection refused" or timeout errors + +**Symptoms:** + +- Data source test fails with network errors +- Queries timeout without returning results + +**Solutions:** + +1. Verify network connectivity from Grafana to Azure endpoints. +2. Check firewall rules allow outbound HTTPS (port 443) to Azure services. +3. For private networks, ensure Private Link or VPN is configured correctly. +4. For Grafana Cloud, configure [Private Data Source Connect](ref:configure-azure-monitor) if accessing private resources. + +### SSL/TLS certificate errors + +**Symptoms:** + +- Certificate validation failures +- SSL handshake errors + +**Solutions:** + +1. Ensure the system time is correct (certificate validation fails with incorrect time). +2. Verify corporate proxy isn't intercepting HTTPS traffic. +3. Check that required CA certificates are installed on the Grafana server. + +## Get additional help + +If you've tried the solutions above and still encounter issues: + +1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues. +1. Review the [Azure Monitor data source GitHub issues](https://github.com/grafana/grafana/issues) for known bugs. +1. Enable debug logging in Grafana to capture detailed error information. +1. Contact Grafana Support if you're an Enterprise, Cloud Pro or Cloud Contracted user. +1. When reporting issues, include: + - Grafana version + - Error messages (redact sensitive information) + - Steps to reproduce + - Relevant configuration (redact credentials) diff --git a/docs/sources/datasources/elasticsearch/_index.md b/docs/sources/datasources/elasticsearch/_index.md index 1143dbdc68b..ebee19b2dcb 100644 --- a/docs/sources/datasources/elasticsearch/_index.md +++ b/docs/sources/datasources/elasticsearch/_index.md @@ -17,16 +17,6 @@ menuTitle: Elasticsearch title: Elasticsearch data source weight: 325 refs: - configuration: - - pattern: /docs/grafana/ - destination: /docs/grafana//setup-grafana/configure-grafana/#sigv4_auth_enabled - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//setup-grafana/configure-grafana/#sigv4_auth_enabled - provisioning-grafana: - - pattern: /docs/grafana/ - destination: /docs/grafana//administration/provisioning/#data-sources - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//administration/provisioning/#data-sources explore: - pattern: /docs/grafana/ destination: /docs/grafana//explore/ @@ -44,12 +34,36 @@ refs: Elasticsearch is a search and analytics engine used for a variety of use cases. You can create many types of queries to visualize logs or metrics stored in Elasticsearch, and annotate graphs with log events stored in Elasticsearch. -The following will help you get started working with Elasticsearch and Grafana: +The following resources will help you get started with Elasticsearch and Grafana: - [What is Elasticsearch?](https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html) -- [Configure the Elasticsearch data source](/docs/grafana/latest/datasources/elasticsearch/configure-elasticsearch-data-source/) -- [Elasticsearch query editor](query-editor/) -- [Elasticsearch template variables](template-variables/) +- [Configure the Elasticsearch data source](https://grafana.com/docs/grafana//datasources/elasticsearch/configure/) +- [Elasticsearch query editor](https://grafana.com/docs/grafana//datasources/elasticsearch/query-editor/) +- [Elasticsearch template variables](https://grafana.com/docs/grafana//datasources/elasticsearch/template-variables/) +- [Elasticsearch annotations](https://grafana.com/docs/grafana//datasources/elasticsearch/annotations/) +- [Elasticsearch alerting](https://grafana.com/docs/grafana//datasources/elasticsearch/alerting/) +- [Troubleshooting issues with the Elasticsearch data source](https://grafana.com/docs/grafana//datasources/elasticsearch/troubleshooting/) + +## Key capabilities + +The Elasticsearch data source supports: + +- **Metrics queries:** Aggregate and visualize numeric data using bucket and metric aggregations. +- **Log queries:** Search, filter, and explore log data with Lucene query syntax. +- **Annotations:** Overlay Elasticsearch events on your dashboard graphs. +- **Alerting:** Create alerts based on Elasticsearch query results. + +## Before you begin + +Before you configure the Elasticsearch data source, you need: + +- An Elasticsearch instance (v7.17+, v8.x, or v9.x) +- Network access from Grafana to your Elasticsearch server +- Appropriate user credentials or API keys with read access + +{{< admonition type="note" >}} +If you use Amazon OpenSearch Service (the successor to Amazon Elasticsearch Service), use the [OpenSearch data source](https://grafana.com/docs/grafana//datasources/opensearch/) instead. +{{< /admonition >}} ## Supported Elasticsearch versions @@ -63,86 +77,18 @@ This data source supports these versions of Elasticsearch: - v8.x - v9.x -Our maintenance policy for Elasticsearch data source is aligned with the [Elastic Product End of Life Dates](https://www.elastic.co/support/eol) and we ensure proper functionality for supported versions. If you are using an Elasticsearch with version that is past its end-of-life (EOL), you can still execute queries, but you will receive a notification in the query builder indicating that the version of Elasticsearch you are using is no longer supported. It's important to note that in such cases, we do not guarantee the correctness of the functionality, and we will not be addressing any related issues. +The Grafana maintenance policy for the Elasticsearch data source aligns with [Elastic Product End of Life Dates](https://www.elastic.co/support/eol). Grafana ensures proper functionality for supported versions only. If you use an EOL version of Elasticsearch, you can still run queries, but the query builder displays a warning. Grafana doesn't guarantee functionality or provide fixes for EOL versions. -## Provision the data source +## Additional resources -You can define and configure the data source in YAML files as part of Grafana's provisioning system. -For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-grafana). +Once you have configured the Elasticsearch data source, you can: -{{< admonition type="note" >}} -The previously used `database` field has now been [deprecated](https://github.com/grafana/grafana/pull/58647). -You should now use the `index` field in `jsonData` to store the index name. -Please see the examples below. -{{< /admonition >}} +- Use [Explore](ref:explore) to run ad-hoc queries against your Elasticsearch data. +- Configure and use [template variables](https://grafana.com/docs/grafana//datasources/elasticsearch/template-variables/) for dynamic dashboards. +- Add [Transformations](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/transform-data/) to process query results. +- [Build dashboards](ref:build-dashboards) to visualize your Elasticsearch data. -### Provisioning examples +## Related data sources -**Basic provisioning** - -```yaml -apiVersion: 1 - -datasources: - - name: Elastic - type: elasticsearch - access: proxy - url: http://localhost:9200 - jsonData: - index: '[metrics-]YYYY.MM.DD' - interval: Daily - timeField: '@timestamp' -``` - -**Provision for logs** - -```yaml -apiVersion: 1 - -datasources: - - name: elasticsearch-v7-filebeat - type: elasticsearch - access: proxy - url: http://localhost:9200 - jsonData: - index: '[filebeat-]YYYY.MM.DD' - interval: Daily - timeField: '@timestamp' - logMessageField: message - logLevelField: fields.level - dataLinks: - - datasourceUid: my_jaeger_uid # Target UID needs to be known - field: traceID - url: '$${__value.raw}' # Careful about the double "$$" because of env var expansion -``` - -## Configure Amazon Elasticsearch Service - -If you use Amazon Elasticsearch Service, you can use Grafana's Elasticsearch data source to visualize data from it. - -If you use an AWS Identity and Access Management (IAM) policy to control access to your Amazon Elasticsearch Service domain, you must use AWS Signature Version 4 (AWS SigV4) to sign all requests to that domain. - -For details on AWS SigV4, refer to the [AWS documentation](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). - -### AWS Signature Version 4 authentication - -To sign requests to your Amazon Elasticsearch Service domain, you can enable SigV4 in Grafana's [configuration](ref:configuration). - -Once AWS SigV4 is enabled, you can configure it on the Elasticsearch data source configuration page. -For more information about AWS authentication options, refer to [AWS authentication](../aws-cloudwatch/aws-authentication/). - -{{< figure src="/static/img/docs/v73/elasticsearch-sigv4-config-editor.png" max-width="500px" class="docs-image--no-shadow" caption="SigV4 configuration for AWS Elasticsearch Service" >}} - -## Query the data source - -You can select multiple metrics and group by multiple terms or filters when using the Elasticsearch query editor. - -For details, see the [query editor documentation](query-editor/). - -## Use template variables - -Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. -Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard. -Grafana refers to such variables as template variables. - -For details, see the [template variables documentation](template-variables/). +- [OpenSearch](https://grafana.com/docs/grafana//datasources/opensearch/) - For Amazon OpenSearch Service. +- [Loki](https://grafana.com/docs/grafana//datasources/loki/) - Grafana's log aggregation system. diff --git a/docs/sources/datasources/elasticsearch/alerting/index.md b/docs/sources/datasources/elasticsearch/alerting/index.md new file mode 100644 index 00000000000..ef002764bda --- /dev/null +++ b/docs/sources/datasources/elasticsearch/alerting/index.md @@ -0,0 +1,144 @@ +--- +aliases: + - ../../data-sources/elasticsearch/alerting/ +description: Using Grafana Alerting with the Elasticsearch data source +keywords: + - grafana + - elasticsearch + - alerting + - alerts +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Alerting +title: Elasticsearch alerting +weight: 550 +refs: + alerting: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/ + create-alert-rule: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/ +--- + +# Elasticsearch alerting + +You can use Grafana Alerting with Elasticsearch to create alerts based on your Elasticsearch data. This allows you to monitor metrics, detect anomalies, and receive notifications when specific conditions are met. + +For general information about Grafana Alerting, refer to [Grafana Alerting](ref:alerting). + +## Before you begin + +Before creating alerts with Elasticsearch, ensure you have: + +- An Elasticsearch data source configured in Grafana +- Appropriate permissions to create alert rules +- Understanding of the metrics you want to monitor + +## Supported query types + +Elasticsearch alerting works best with **metrics queries** that return time series data. To create a valid alert query: + +- Use a **Date histogram** as the last bucket aggregation (under **Group by**) +- Select appropriate metric aggregations (Count, Average, Sum, Min, Max, etc.) + +Queries that return time series data allow Grafana to evaluate values over time and trigger alerts when thresholds are crossed. + +### Query types and alerting compatibility + +| Query type | Alerting support | Notes | +| ------------------------------ | ---------------- | ----------------------------------------------------------- | +| Metrics with Date histogram | ✅ Full support | Recommended for alerting | +| Metrics without Date histogram | ⚠️ Limited | May not evaluate correctly over time | +| Logs | ❌ Not supported | Use metrics queries instead | +| Raw data | ❌ Not supported | Use metrics queries instead | +| Raw document (deprecated) | ❌ Not supported | Deprecated since Grafana v10.1. Use metrics queries instead | + +## Create an alert rule + +To create an alert rule using Elasticsearch: + +1. Navigate to **Alerting** > **Alert rules**. +1. Click **New alert rule**. +1. Enter a name for the alert rule. +1. Select your **Elasticsearch** data source. +1. Build your query using the query editor: + - Add metric aggregations (for example, Average, Count, Sum) + - Add a Date histogram under **Group by** + - Optionally add filters using Lucene query syntax +1. Configure the alert condition (for example, when the average is above a threshold). +1. Set the evaluation interval and pending period. +1. Configure notifications and labels. +1. Click **Save rule**. + +For detailed instructions, refer to [Create a Grafana-managed alert rule](ref:create-alert-rule). + +## Example alert queries + +The following examples show common alerting scenarios with Elasticsearch. + +### Alert on high error count + +Monitor the number of error-level log entries: + +1. **Query:** `level:error` +1. **Metric:** Count +1. **Group by:** Date histogram (interval: 1m) +1. **Condition:** When count is above 100 + +### Alert on average response time + +Monitor API response times: + +1. **Query:** `type:api_request` +1. **Metric:** Average on field `response_time` +1. **Group by:** Date histogram (interval: 5m) +1. **Condition:** When average is above 500 (milliseconds) + +### Alert on unique user count drop + +Detect drops in active users: + +1. **Query:** `*` (all documents) +1. **Metric:** Unique count on field `user_id` +1. **Group by:** Date histogram (interval: 1h) +1. **Condition:** When unique count is below 100 + +## Limitations + +When using Elasticsearch with Grafana Alerting, be aware of the following limitations: + +### Template variables not supported + +Alert queries cannot contain template variables. Grafana evaluates alert rules on the backend without dashboard context, so variables like `$hostname` or `$environment` won't be resolved. + +If your dashboard query uses template variables, create a separate query for alerting with hard coded values. + +### Logs queries not supported + +Queries using the **Logs** metric type cannot be used for alerting. Convert your query to use metric aggregations with a Date histogram instead. + +### Query complexity + +Complex queries with many nested aggregations may timeout or fail to evaluate. Simplify queries for alerting by: + +- Reducing the number of bucket aggregations +- Using appropriate time intervals +- Adding filters to limit the data scanned + +## Best practices + +Follow these best practices when creating Elasticsearch alerts: + +- **Use specific filters:** Add Lucene query filters to focus on relevant data and improve query performance. +- **Choose appropriate intervals:** Match the Date histogram interval to your evaluation frequency. +- **Test queries first:** Verify your query returns expected results in Explore before creating an alert. +- **Set realistic thresholds:** Base alert thresholds on historical data patterns. +- **Use meaningful names:** Give alert rules descriptive names that indicate what they monitor. diff --git a/docs/sources/datasources/elasticsearch/annotations/index.md b/docs/sources/datasources/elasticsearch/annotations/index.md new file mode 100644 index 00000000000..788cfe15ff6 --- /dev/null +++ b/docs/sources/datasources/elasticsearch/annotations/index.md @@ -0,0 +1,124 @@ +--- +aliases: + - ../../data-sources/elasticsearch/annotations/ +description: Using annotations with Elasticsearch in Grafana +keywords: + - grafana + - elasticsearch + - annotations + - events +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Annotations +title: Elasticsearch annotations +weight: 500 +refs: + annotate-visualizations: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ +--- + +# Elasticsearch annotations + +Annotations overlay event data on your dashboard graphs, helping you correlate log events with metrics. +You can use Elasticsearch as a data source for annotations to display events such as deployments, alerts, or other significant occurrences on your visualizations. + +For general information about annotations, refer to [Annotate visualizations](ref:annotate-visualizations). + +## Before you begin + +Before creating Elasticsearch annotations, ensure you have: + +- An Elasticsearch data source configured in Grafana +- Documents in Elasticsearch containing event data with timestamp fields +- Read access to the Elasticsearch index containing your events + +## Create an annotation query + +To add an Elasticsearch annotation to your dashboard: + +1. Navigate to your dashboard and click **Dashboard settings** (gear icon). +1. Select **Annotations** in the left menu. +1. Click **Add annotation query**. +1. Enter a **Name** for the annotation. +1. Select your **Elasticsearch** data source from the **Data source** drop-down. +1. Configure the annotation query and field mappings. +1. Click **Save dashboard**. + +## Query + +Use the query field to filter which Elasticsearch documents appear as annotations. The query uses [Lucene query syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax). + +**Examples:** + +| Query | Description | +| ---------------------------------------- | ---------------------------------------------------- | +| `*` | Matches all documents. | +| `type:deployment` | Shows only deployment events. | +| `level:error OR level:critical` | Shows error and critical events. | +| `service:api AND environment:production` | Shows events for a specific service and environment. | +| `tags:release` | Shows events tagged as releases. | + +You can use template variables in your annotation queries. For example, `service:$service` filters annotations based on the selected service variable. + +## Field mappings + +Field mappings tell Grafana which Elasticsearch fields contain the annotation data. + +### Time + +The **Time** field specifies which field contains the annotation timestamp. + +- **Default:** `@timestamp` +- **Format:** The field must contain a date value that Elasticsearch recognizes. + +### Time End + +The **Time End** field specifies a field containing the end time for range annotations. Range annotations display as a shaded region on the graph instead of a single vertical line. + +- **Default:** Empty (single-point annotations) +- **Use case:** Display maintenance windows, incidents, or any event with a duration. + +### Text + +The **Text** field specifies which field contains the annotation description displayed when you hover over the annotation. + +- **Default:** `tags` +- **Tip:** Use a descriptive field like `message`, `description`, or `summary`. + +### Tags + +The **Tags** field specifies which field contains tags for the annotation. Tags help categorize and filter annotations. + +- **Default:** Empty +- **Format:** The field can contain either a comma-separated string or an array of strings. + +## Example: Deployment annotations + +To display deployment events as annotations: + +1. Create an annotation query with the following settings: + - **Query:** `type:deployment` + - **Time:** `@timestamp` + - **Text:** `message` + - **Tags:** `environment` + +This configuration displays deployment events with their messages as the annotation text and environments as tags. + +## Example: Range annotations for incidents + +To display incidents with duration: + +1. Create an annotation query with the following settings: + - **Query:** `type:incident` + - **Time:** `start_time` + - **Time End:** `end_time` + - **Text:** `description` + - **Tags:** `severity` + +This configuration displays incidents as shaded regions from their start time to end time. diff --git a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md deleted file mode 100644 index 6b145841bbf..00000000000 --- a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -aliases: - - ../data-sources/elasticsearch/ - - ../features/datasources/elasticsearch/ -description: Guide for configuring the Elasticsearch data source in Grafana -keywords: - - grafana - - elasticsearch - - guide - - data source -labels: - products: - - cloud - - enterprise - - oss -menuTitle: Configure Elasticsearch -title: Configure the Elasticsearch data source -weight: 200 -refs: - administration-documentation: - - pattern: /docs/grafana/ - destination: /docs/grafana//administration/data-source-management/ - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//administration/data-source-management/ - supported-expressions: - - pattern: /docs/grafana/ - destination: /docs/grafana//explore/logs-integration/#log-level - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//explore/logs-integration/#log-level - query-and-transform-data: - - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/ - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ - provisioning-data-source: - - pattern: /docs/grafana/ - destination: /docs/grafana//datasources/elasticsearch/#provision-the-data-source - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/elasticsearch/#provision-the-data-source ---- - -# Configure the Elasticsearch data source - -Grafana ships with built-in support for Elasticsearch. -You can create a variety of queries to visualize logs or metrics stored in Elasticsearch, and annotate graphs with log events stored in Elasticsearch. - -For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:administration-documentation). - -Only users with the organization `administrator` role can add data sources. -Administrators can also [configure the data source via YAML](ref:provisioning-data-source) with Grafana's provisioning system. - -## Configuring permissions - -When Elasticsearch security features are enabled, it is essential to configure the necessary cluster privileges to ensure seamless operation. Below is a list of the required privileges along with their purposes: - -- **monitor** - Necessary to retrieve the version information of the connected Elasticsearch instance. -- **view_index_metadata** - Required for accessing mapping definitions of indices. -- **read** - Grants the ability to perform search and retrieval operations on indices. This is essential for querying and extracting data from the cluster. - -## Add the data source - -To add the Elasticsearch data source, complete the following steps: - -1. Click **Connections** in the left-side menu. -1. Under **Connections**, click **Add new connection**. -1. Enter `Elasticsearch` in the search bar. -1. Click **Elasticsearch** under the **Data source** section. -1. Click **Add new data source** in the upper right. - -You will be taken to the **Settings** tab where you will set up your Elasticsearch configuration. - -## Configuration options - -The following is a list of configuration options for Elasticsearch. - -The first option to configure is the name of your connection: - -- **Name** - The data source name. This is how you refer to the data source in panels and queries. Examples: elastic-1, elasticsearch_metrics. - -- **Default** - Toggle to select as the default data source option. When you go to a dashboard panel or Explore, this will be the default selected data source. - -## Connection - -Connect the Elasticsearch data source by specifying a URL. - -- **URL** - The URL of your Elasticsearch server. If your Elasticsearch server is local, use `http://localhost:9200`. If it is on a server within a network, this is the URL with the port where you are running Elasticsearch. Example: `http://elasticsearch.example.orgname:9200`. - -## Authentication - -There are several authentication methods you can choose in the Authentication section. -Select one of the following authentication methods from the dropdown menu. - -- **Basic authentication** - The most common authentication method. Use your `data source` user name and `data source` password to connect. - -- **Forward OAuth identity** - Forward the OAuth access token (and the OIDC ID token if available) of the user querying the data source. - -- **No authentication** - Make the data source available without authentication. Grafana recommends using some type of authentication method. - - - -### TLS settings - -{{< admonition type="note" >}} -Use TLS (Transport Layer Security) for an additional layer of security when working with Elasticsearch. For information on setting up TLS encryption with Elasticsearch see [Configure TLS](https://www.elastic.co/guide/en/elasticsearch/reference/8.8/configuring-tls.html#configuring-tls). You must add TLS settings to your Elasticsearch configuration file **prior** to setting these options in Grafana. -{{< /admonition >}} - -- **Add self-signed certificate** - Check the box to authenticate with a CA certificate. Follow the instructions of the CA (Certificate Authority) to download the certificate file. Required for verifying self-signed TLS certificates. - -- **TLS client authentication** - Check the box to authenticate with the TLS client, where the server authenticates the client. Add the `Server name`, `Client certificate` and `Client key`. The **ServerName** is used to verify the hostname on the returned certificate. The **Client certificate** can be generated from a Certificate Authority (CA) or be self-signed. The **Client key** can also be generated from a Certificate Authority (CA) or be self-signed. The client key encrypts the data between client and server. - -- **Skip TLS certificate validation** - Check the box to bypass TLS certificate validation. Skipping TLS certificate validation is not recommended unless absolutely necessary or for testing purposes. - -### HTTP headers - -Click **+ Add header** to add one or more HTTP headers. HTTP headers pass additional context and metadata about the request/response. - -- **Header** - Add a custom header. This allows custom headers to be passed based on the needs of your Elasticsearch instance. - -- **Value** - The value of the header. - -## Additional settings - -Additional settings are optional settings that can be configured for more control over your data source. - -### Advanced HTTP settings - -- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. - -- **Timeout** - The HTTP request timeout. This must be in seconds. There is no default, so this setting is up to you. - -### Elasticsearch details - -The following settings are specific to the Elasticsearch data source. - -- **Index name** - Use the index settings to specify a default for the `time field` and your Elasticsearch index's name. You can use a time pattern, for example `[logstash-]YYYY.MM.DD`, or a wildcard for the index name. When specifying a time pattern, the fixed part(s) of the pattern should be wrapped in square brackets. - -- **Pattern** - Select the matching pattern if using one in your index name. Options include: - - no pattern - - hourly - - daily - - weekly - - monthly - - yearly - -Only select a pattern option if you have specified a time pattern in the Index name field. - -- **Time field name** - Name of the time field. The default value is @timestamp. You can enter a different name. - -- **Max concurrent shard requests** - Sets the number of shards being queried at the same time. The default is `5`. For more information on shards see [Elasticsearch's documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/scalability.html#scalability). - -- **Min time interval** - Defines a lower limit for the auto group-by time interval. This value **must** be formatted as a number followed by a valid time identifier: - - | Identifier | Description | - | ---------- | ----------- | - | `y` | year | - | `M` | month | - | `w` | week | - | `d` | day | - | `h` | hour | - | `m` | minute | - | `s` | second | - | `ms` | millisecond | - -We recommend setting this value to match your Elasticsearch write frequency. -For example, set this to `1m` if Elasticsearch writes data every minute. - -You can also override this setting in a dashboard panel under its data source options. The default is `10s`. - -- **X-Pack enabled** - Toggle to enable `X-Pack`-specific features and options, which provide the [query editor](../query-editor/) with additional aggregations, such as `Rate` and `Top Metrics`. - -- **Include frozen indices** - Toggle on when the `X-Pack enabled` setting is active. Includes frozen indices in searches. You can configure Grafana to include [frozen indices](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/frozen-indices.html) when performing search requests. - -{{< admonition type="note" >}} -Frozen indices are [deprecated in Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/frozen-indices.html) since v7.14. -{{< /admonition >}} - -### Logs - -In this section you can configure which fields the data source uses for log messages and log levels. - -- **Message field name:** - Grabs the actual log message from the default source. - -- **Level field name:** - Name of the field with log level/severity information. When a level label is specified, the value of this label is used to determine the log level and update the color of each log line accordingly. If the log doesn’t have a specified level label, we try to determine if its content matches any of the [supported expressions](ref:supported-expressions). The first match always determines the log level. If Grafana cannot infer a log-level field, it will be visualized with an unknown log level. - -### Data links - -Data links create a link from a specified field that can be accessed in Explore's logs view. You can add multiple data links by clicking **+ Add**. - -Each data link configuration consists of: - -- **Field** - Sets the name of the field used by the data link. - -- **URL/query** - Sets the full link URL if the link is external. If the link is internal, this input serves as a query for the target data source.
In both cases, you can interpolate the value from the field with the `${__value.raw }` macro. - -- **URL Label** (Optional) - Sets a custom display label for the link. The link label defaults to the full external URL or name of the linked internal data source and is overridden by this setting. - -- **Internal link** - Toggle on to set an internal link. For an internal link, you can select the target data source with a data source selector. This supports only tracing data sources. - -## Private data source connect (PDC) and Elasticsearch - -Use private data source connect (PDC) to connect to and query data within a secure network without opening that network to inbound traffic from Grafana Cloud. See [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) for more information on how PDC works and [Configure Grafana private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc) for steps on setting up a PDC connection. - -If you use PDC with SIGv4 (AWS Signature Version 4 Authentication), the PDC agent must allow internet egress to`sts..amazonaws.com:443`. - -- **Private data source connect** - Click in the box to set the default PDC connection from the dropdown menu or create a new connection. - -Once you have configured your Elasticsearch data source options, click **Save & test** at the bottom to test out your data source connection. You can also remove a connection by clicking **Delete**. diff --git a/docs/sources/datasources/elasticsearch/configure/index.md b/docs/sources/datasources/elasticsearch/configure/index.md new file mode 100644 index 00000000000..d76a90855ea --- /dev/null +++ b/docs/sources/datasources/elasticsearch/configure/index.md @@ -0,0 +1,377 @@ +--- +aliases: + - ../configure-elasticsearch-data-source/ +description: Guide for configuring the Elasticsearch data source in Grafana +keywords: + - grafana + - elasticsearch + - guide + - data source +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Configure +title: Configure the Elasticsearch data source +weight: 200 +refs: + administration-documentation: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/data-source-management/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/data-source-management/ + supported-expressions: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/logs-integration/#log-level + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//explore/logs-integration/#log-level + query-and-transform-data: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/query-transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ + provisioning-data-source: + - pattern: /docs/grafana/ + destination: /docs/grafana//datasources/elasticsearch/configure/#provision-the-data-source + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/elasticsearch/configure/#provision-the-data-source + configuration: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/#sigv4_auth_enabled + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/#sigv4_auth_enabled + provisioning-grafana: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/provisioning/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/provisioning/ +--- + +# Configure the Elasticsearch data source + +Grafana ships with built-in support for Elasticsearch. +You can create a variety of queries to visualize logs or metrics stored in Elasticsearch, and annotate graphs with log events stored in Elasticsearch. + +For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:administration-documentation). +Administrators can also [configure the data source via YAML](ref:provisioning-data-source) with Grafana's provisioning system. + +## Before you begin + +To configure the Elasticsearch data source, you need: + +- **Grafana administrator permissions:** Only users with the organization `administrator` role can add data sources. +- **A supported Elasticsearch version:** v7.17 or later, v8.x, or v9.x. Elastic Cloud Serverless isn't supported. +- **Elasticsearch server URL:** The HTTP or HTTPS endpoint for your Elasticsearch instance, including the port (default: `9200`). +- **Authentication credentials:** Depending on your Elasticsearch security configuration, you need one of the following: + - Username and password for basic authentication + - API key + - No credentials (if Elasticsearch security is disabled) +- **Network access:** Grafana must be able to reach your Elasticsearch server. For Grafana Cloud, consider using [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) if your Elasticsearch instance is in a private network. + +## Elasticsearch permissions + +When Elasticsearch security features are enabled, you must configure the following cluster privileges for the user or API key that Grafana uses to connect: + +- **monitor** - Necessary to retrieve the version information of the connected Elasticsearch instance. +- **view_index_metadata** - Required for accessing mapping definitions of indices. +- **read** - Grants the ability to perform search and retrieval operations on indices. This is essential for querying and extracting data from the cluster. + +## Add the data source + +To add the Elasticsearch data source, complete the following steps: + +1. Click **Connections** in the left-side menu. +1. Under **Connections**, click **Add new connection**. +1. Enter `Elasticsearch` in the search bar. +1. Click **Elasticsearch** under the **Data source** section. +1. Click **Add new data source** in the upper right. + +You will be taken to the **Settings** tab where you will set up your Elasticsearch configuration. + +## Configuration options + +Configure the following basic settings for the Elasticsearch data source: + +- **Name** - The data source name. This is how you refer to the data source in panels and queries. Examples: `elastic-1`, `elasticsearch_metrics`. + +- **Default** - Toggle on to make this the default data source. New panels and Explore queries use the default data source. + +## Connection + +- **URL** - The URL of your Elasticsearch server, including the port. Examples: `http://localhost:9200`, `http://elasticsearch.example.com:9200`. + +## Authentication + +Select an authentication method from the drop-down menu: + +- **Basic authentication** - Enter the username and password for your Elasticsearch user. + +- **Forward OAuth identity** - Forward the OAuth access token (and the OIDC ID token if available) of the user querying the data source. + +- **No authentication** - Connect without credentials. Only use this option if your Elasticsearch instance doesn't require authentication. + +### API key authentication + +To authenticate using an Elasticsearch API key, select **No authentication** and configure the API key using HTTP headers: + +1. In the **HTTP headers** section, click **+ Add header**. +1. Set **Header** to `Authorization`. +1. Set **Value** to `ApiKey `, replacing `` with your base64-encoded Elasticsearch API key. + +For information about creating API keys, refer to the [Elasticsearch API keys documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html). + +### Amazon Elasticsearch Service + +If you use Amazon Elasticsearch Service, you can use Grafana's Elasticsearch data source to visualize data from it. + +If you use an AWS Identity and Access Management (IAM) policy to control access to your Amazon Elasticsearch Service domain, you must use AWS Signature Version 4 (AWS SigV4) to sign all requests to that domain. + +For details on AWS SigV4, refer to the [AWS documentation](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). + +To sign requests to your Amazon Elasticsearch Service domain, you can enable SigV4 in Grafana's [configuration](ref:configuration). + +Once AWS SigV4 is enabled, you can configure it on the Elasticsearch data source configuration page. +For more information about AWS authentication options, refer to [AWS authentication](https://grafana.com/docs/grafana//datasources/aws-cloudwatch/aws-authentication/). + +{{< figure src="/static/img/docs/v73/elasticsearch-sigv4-config-editor.png" max-width="500px" class="docs-image--no-shadow" caption="SigV4 configuration for AWS Elasticsearch Service" >}} + +### TLS settings + +{{< admonition type="note" >}} +Use TLS (Transport Layer Security) for an additional layer of security when working with Elasticsearch. For information on setting up TLS encryption with Elasticsearch, refer to [Configure TLS](https://www.elastic.co/guide/en/elasticsearch/reference/8.8/configuring-tls.html#configuring-tls). You must add TLS settings to your Elasticsearch configuration file **prior** to setting these options in Grafana. +{{< /admonition >}} + +- **Add self-signed certificate** - Check the box to authenticate with a CA certificate. Follow the instructions of the CA (Certificate Authority) to download the certificate file. Required for verifying self-signed TLS certificates. + +- **TLS client authentication** - Check the box to authenticate with the TLS client, where the server authenticates the client. Add the `Server name`, `Client certificate` and `Client key`. The **ServerName** is used to verify the hostname on the returned certificate. The **Client certificate** can be generated from a Certificate Authority (CA) or be self-signed. The **Client key** can also be generated from a Certificate Authority (CA) or be self-signed. The client key encrypts the data between client and server. + +- **Skip TLS certificate validation** - Check the box to bypass TLS certificate validation. Skipping TLS certificate validation is not recommended unless absolutely necessary or for testing purposes. + +### HTTP headers + +Click **+ Add header** to add one or more HTTP headers. HTTP headers pass additional context and metadata about the request/response. + +- **Header** - Add a custom header. This allows custom headers to be passed based on the needs of your Elasticsearch instance. + +- **Value** - The value of the header. + +## Additional settings + +Additional settings are optional settings that can be configured for more control over your data source. + +### Advanced HTTP settings + +- **Allowed cookies** - Specify cookies by name that should be forwarded to the data source. The Grafana proxy deletes all forwarded cookies by default. + +- **Timeout** - The HTTP request timeout. This must be in seconds. There is no default, so this setting is up to you. + +### Elasticsearch details + +The following settings are specific to the Elasticsearch data source. + +- **Index name** - The name of your Elasticsearch index. You can use the following formats: + - **Wildcard patterns** - Use `*` to match multiple indices. Examples: `logs-*`, `metrics-*`, `filebeat-*`. + - **Time patterns** - Use date placeholders for time-based indices. Wrap the fixed portion in square brackets. Examples: `[logstash-]YYYY.MM.DD`, `[metrics-]YYYY.MM`. + - **Specific index** - Enter the exact index name. Example: `application-logs`. + +- **Pattern** - Select the matching pattern if you use a time pattern in your index name. Options include: + - no pattern + - hourly + - daily + - weekly + - monthly + - yearly + +Only select a pattern option if you have specified a time pattern in the Index name field. + +- **Time field name** - Name of the time field. The default value is `@timestamp`. You can enter a different name. + +- **Max concurrent shard requests** - Sets the number of shards being queried at the same time. The default is `5`. For more information on shards, refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/scalability.html#scalability). + +- **Min time interval** - Defines a lower limit for the auto group-by time interval. This value **must** be formatted as a number followed by a valid time identifier: + + | Identifier | Description | + | ---------- | ----------- | + | `y` | year | + | `M` | month | + | `w` | week | + | `d` | day | + | `h` | hour | + | `m` | minute | + | `s` | second | + | `ms` | millisecond | + +We recommend setting this value to match your Elasticsearch write frequency. +For example, set this to `1m` if Elasticsearch writes data every minute. + +You can also override this setting in a dashboard panel under its data source options. The default is `10s`. + +- **X-Pack enabled** - Toggle to enable `X-Pack`-specific features and options, which provide the [query editor](https://grafana.com/docs/grafana//datasources/elasticsearch/query-editor/) with additional aggregations, such as `Rate` and `Top Metrics`. + +- **Include frozen indices** - Toggle on when the `X-Pack enabled` setting is active. Includes frozen indices in searches. You can configure Grafana to include [frozen indices](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/frozen-indices.html) when performing search requests. + +{{< admonition type="note" >}} +Frozen indices are [deprecated in Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/frozen-indices.html) since v7.14. +{{< /admonition >}} + +### Logs + +Configure which fields the data source uses for log messages and log levels. + +- **Message field name** - The field that contains the log message content. + +- **Level field name** - The field that contains log level or severity information. When specified, Grafana uses this field to determine the log level and color-code each log line. If the log doesn't have a level field, Grafana tries to match the content against [supported expressions](ref:supported-expressions). If Grafana can't determine the log level, it displays as unknown. + +### Data links + +Data links create a link from a specified field that can be accessed in Explore's logs view. You can add multiple data links by clicking **+ Add**. + +Each data link configuration consists of: + +- **Field** - Sets the name of the field used by the data link. + +- **URL/query** - Sets the full link URL if the link is external. If the link is internal, this input serves as a query for the target data source.
In both cases, you can interpolate the value from the field with the `${__value.raw }` macro. + +- **URL Label** (Optional) - Sets a custom display label for the link. The link label defaults to the full external URL or name of the linked internal data source and is overridden by this setting. + +- **Internal link** - Toggle on to set an internal link. For an internal link, you can select the target data source with a data source selector. This supports only tracing data sources. + +## Private data source connect (PDC) and Elasticsearch + +Use private data source connect (PDC) to connect to and query data within a secure network without opening that network to inbound traffic from Grafana Cloud. Refer to [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) for more information on how PDC works and [Configure Grafana private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc) for steps on setting up a PDC connection. + +If you use PDC with SigV4 (AWS Signature Version 4 Authentication), the PDC agent must allow internet egress to `sts..amazonaws.com:443`. + +- **Private data source connect** - Click in the box to set the default PDC connection from the drop-down menu or create a new connection. + +Once you have configured your Elasticsearch data source options, click **Save & test** to test the connection. A successful connection displays the following message: + +`Elasticsearch data source is healthy.` + +## Provision the data source + +You can define and configure the data source in YAML files as part of Grafana's provisioning system. +For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-grafana). + +{{< admonition type="note" >}} +The previously used `database` field has now been [deprecated](https://github.com/grafana/grafana/pull/58647). +Use the `index` field in `jsonData` to store the index name. +Refer to the examples below. +{{< /admonition >}} + +### Basic provisioning + +```yaml +apiVersion: 1 + +datasources: + - name: Elastic + type: elasticsearch + access: proxy + url: http://localhost:9200 + jsonData: + index: '[metrics-]YYYY.MM.DD' + interval: Daily + timeField: '@timestamp' +``` + +### Provision for logs + +```yaml +apiVersion: 1 + +datasources: + - name: elasticsearch-v7-filebeat + type: elasticsearch + access: proxy + url: http://localhost:9200 + jsonData: + index: '[filebeat-]YYYY.MM.DD' + interval: Daily + timeField: '@timestamp' + logMessageField: message + logLevelField: fields.level + dataLinks: + - datasourceUid: my_jaeger_uid # Target UID needs to be known + field: traceID + url: '$${__value.raw}' # Careful about the double "$$" because of env var expansion +``` + +## Provision the data source using Terraform + +You can provision the Elasticsearch data source using [Terraform](https://www.terraform.io/) with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). + +For more information about provisioning resources with Terraform, refer to the [Grafana as code using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/) documentation. + +### Basic Terraform example + +The following example creates a basic Elasticsearch data source for metrics: + +```hcl +resource "grafana_data_source" "elasticsearch" { + name = "Elasticsearch" + type = "elasticsearch" + url = "http://localhost:9200" + + json_data_encoded = jsonencode({ + index = "[metrics-]YYYY.MM.DD" + interval = "Daily" + timeField = "@timestamp" + }) +} +``` + +### Terraform example for logs + +The following example creates an Elasticsearch data source configured for logs with a data link to Jaeger: + +```hcl +resource "grafana_data_source" "elasticsearch_logs" { + name = "Elasticsearch Logs" + type = "elasticsearch" + url = "http://localhost:9200" + + json_data_encoded = jsonencode({ + index = "[filebeat-]YYYY.MM.DD" + interval = "Daily" + timeField = "@timestamp" + logMessageField = "message" + logLevelField = "fields.level" + dataLinks = [ + { + datasourceUid = grafana_data_source.jaeger.uid + field = "traceID" + url = "$${__value.raw}" + } + ] + }) +} +``` + +### Terraform example with basic authentication + +The following example includes basic authentication: + +```hcl +resource "grafana_data_source" "elasticsearch_auth" { + name = "Elasticsearch" + type = "elasticsearch" + url = "http://localhost:9200" + + basic_auth_enabled = true + basic_auth_username = "elastic_user" + + secure_json_data_encoded = jsonencode({ + basicAuthPassword = var.elasticsearch_password + }) + + json_data_encoded = jsonencode({ + index = "[metrics-]YYYY.MM.DD" + interval = "Daily" + timeField = "@timestamp" + }) +} +``` + +For all available configuration options, refer to the [Grafana provider data source resource documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source). diff --git a/docs/sources/datasources/elasticsearch/query-editor/index.md b/docs/sources/datasources/elasticsearch/query-editor/index.md index fa20353a395..c5c7bf91abf 100644 --- a/docs/sources/datasources/elasticsearch/query-editor/index.md +++ b/docs/sources/datasources/elasticsearch/query-editor/index.md @@ -30,7 +30,7 @@ refs: # Elasticsearch query editor Grafana provides a query editor for Elasticsearch. Elasticsearch queries are in Lucene format. -See [Lucene query syntax](https://www.elastic.co/guide/en/kibana/current/lucene-query.html) and [Query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/query-dsl-query-string-query.html#query-string-syntax) if you are new to working with Lucene queries in Elasticsearch. +For more information about query syntax, refer to [Lucene query syntax](https://www.elastic.co/guide/en/kibana/current/lucene-query.html) and [Query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax). {{< admonition type="note" >}} When composing Lucene queries, ensure that you use uppercase boolean operators: `AND`, `OR`, and `NOT`. Lowercase versions of these operators are not supported by the Lucene query syntax. @@ -38,17 +38,17 @@ When composing Lucene queries, ensure that you use uppercase boolean operators: {{< figure src="/static/img/docs/elasticsearch/elastic-query-editor-10.1.png" max-width="800px" class="docs-image--no-shadow" caption="Elasticsearch query editor" >}} -For general documentation on querying data sources in Grafana, including options and functions common to all query editors, see [Query and transform data](ref:query-and-transform-data). +For general documentation on querying data sources in Grafana, including options and functions common to all query editors, refer to [Query and transform data](ref:query-and-transform-data). ## Aggregation types Elasticsearch groups aggregations into three categories: -- **Bucket** - Bucket aggregations don't calculate metrics, they create buckets of documents based on field values, ranges and a variety of other criteria. See [Bucket aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html) for additional information. Use bucket aggregations under `Group by` when creating a metrics query in the query builder. +- **Bucket** - Bucket aggregations don't calculate metrics, they create buckets of documents based on field values, ranges and a variety of other criteria. Refer to [Bucket aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html) for additional information. Use bucket aggregations under `Group by` when creating a metrics query in the query builder. -- **Metrics** - Metrics aggregations perform calculations such as sum, average, min, etc. They can be single-value or multi-value. See [Metrics aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html) for additional information. Use metrics aggregations in the metrics query type in the query builder. +- **Metrics** - Metrics aggregations perform calculations such as sum, average, min, etc. They can be single-value or multi-value. Refer to [Metrics aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html) for additional information. Use metrics aggregations in the metrics query type in the query builder. -- **Pipeline** - Elasticsearch pipeline aggregations work with inputs or metrics created from other aggregations (not documents or fields). There are parent and sibling and sibling pipeline aggregations. See [Pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-pipeline.html) for additional information. +- **Pipeline** - Pipeline aggregations work on the output of other aggregations rather than on documents or fields. Refer to [Pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html) for additional information. ## Select a query type @@ -56,44 +56,51 @@ There are three types of queries you can create with the Elasticsearch query bui ### Metrics query type -Metrics queries aggregate data and produce a variety of calculations such as count, min, max, etc. Click on the metric box to view a list of options in the dropdown menu. The default is `count`. +Metrics queries aggregate data and produce calculations such as count, min, max, and more. Click the metric box to view options in the drop-down menu. The default is `count`. - **Alias** - Aliasing only applies to **time series queries**, where the last group is `date histogram`. This is ignored for any other type of query. - **Metric** - Metrics aggregations include: - - count - see [Value count aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-valuecount-aggregation.html) - - average - see [Avg aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-rate-aggregation.html) - - sum - see [Sum aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html) - - max - see [Max aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-max-aggregation.html) - - min - see [Min aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-min-aggregation.html) - - extended stats - see [Extended stats aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) - - percentiles - see [Percentiles aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-percentile-aggregation.html) - - unique count - see [Cardinality aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-cardinality-aggregation.html) - - top metrics - see [Top metrics aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-top-metrics.html) - - rate - see [Rate aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-aggregations-metrics-rate-aggregation.html) + - count - refer to [Value count aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html) + - average - refer to [Avg aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) + - sum - refer to [Sum aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html) + - max - refer to [Max aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html) + - min - refer to [Min aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html) + - extended stats - refer to [Extended stats aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) + - percentiles - refer to [Percentiles aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html) + - unique count - refer to [Cardinality aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html) + - top metrics - refer to [Top metrics aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-metrics.html) + - rate - refer to [Rate aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-rate-aggregation.html) + +- **Pipeline aggregations** - Pipeline aggregations work on the output of other aggregations rather than on documents. The following pipeline aggregations are available: + - moving function - Calculates a value based on a sliding window of aggregated values. Refer to [Moving function aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movfn-aggregation.html). + - derivative - Calculates the derivative of a metric. Refer to [Derivative aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html). + - cumulative sum - Calculates the cumulative sum of a metric. Refer to [Cumulative sum aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html). + - serial difference - Calculates the difference between values in a time series. Refer to [Serial differencing aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html). + - bucket script - Executes a script on metric values from other aggregations. Refer to [Bucket script aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). You can select multiple metrics and group by multiple terms or filters when using the Elasticsearch query editor. Use the **+ sign** to the right to add multiple metrics to your query. Click on the **eye icon** next to **Metric** to hide metrics, and the **garbage can icon** to remove metrics. -- **Group by options** - Create multiple group by options when constructing your Elasticsearch query. Date histogram is the default option. Below is a list of options in the dropdown menu. - - terms - see [Terms aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html). - - filter - see [Filter aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html). - - geo hash grid - see [Geohash grid aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html). - - date histogram - for time series queries. See [Date histogram aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html). - - histogram - Depicts frequency distributions. See [Histogram aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html). - - nested (experimental) - See [Nested aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html). +- **Group by options** - Create multiple group by options when constructing your Elasticsearch query. Date histogram is the default option. The following options are available in the drop-down menu: + - terms - refer to [Terms aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html). + - filter - refer to [Filter aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html). + - geo hash grid - refer to [Geohash grid aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html). + - date histogram - for time series queries. Refer to [Date histogram aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html). + - histogram - Depicts frequency distributions. Refer to [Histogram aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html). + - nested (experimental) - Refer to [Nested aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html). Each group by option will have a different subset of options to further narrow your query. The following options are specific to the **date histogram** bucket aggregation option. -- **Time field** - Depicts date data options. The default option can be specified when configuring the Elasticsearch data source in the **Time field name** under the [**Elasticsearch details**](/docs/grafana/latest/datasources/elasticsearch/configure-elasticsearch-data-source/#elasticsearch-details) section. Otherwise **@timestamp** field will be used as a default option. -- **Interval** - Group by a type of interval. There are option to choose from the dropdown menu to select seconds, minutes, hours or day. You can also add a custom interval such as `30d` (30 days). `Auto` is the default option. -- **Min doc count** - The minimum amount of data to include in your query. The default is `0`. -- **Thin edges** - Select to trim edges on the time series data points. The default is `0`. -- **Offset** - Changes the start value of each bucket by the specified positive(+) or negative (-) offset duration. Examples include `1h` for 1 hour, `5s` for 5 seconds or `1d` for 1 day. -- **Timezone** - Select a timezone from the dropdown menu. The default is `Coordinated universal time`. +- **Time field** - The field used for time-based queries. The default can be set when configuring the data source in the **Time field name** setting under [Elasticsearch details](https://grafana.com/docs/grafana//datasources/elasticsearch/configure/#elasticsearch-details). The default is `@timestamp`. +- **Interval** - The time interval for grouping data. Select from the drop-down menu or enter a custom interval such as `30d` (30 days). The default is `Auto`. +- **Min doc count** - The minimum number of documents required to include a bucket. The default is `0`. +- **Trim edges** - Removes partial buckets at the edges of the time range. The default is `0`. +- **Offset** - Shifts the start of each bucket by the specified duration. Use positive (`+`) or negative (`-`) values. Examples: `1h`, `5s`, `1d`. +- **Timezone** - The timezone for date calculations. The default is `Coordinated Universal Time`. Configure the following options for the **terms** bucket aggregation option: @@ -101,7 +108,7 @@ Configure the following options for the **terms** bucket aggregation option: - **Size** - Limits the number of documents, or size of the data set. You can set a custom number or `no limit`. - **Min doc count** - The minimum amount of data to include in your query. The default is `0`. - **Order by** - Order terms by `term value`, `doc count` or `count`. -- **Missing** - Defines how documents missing a value should be treated. Missing values are ignored by default, but they can be treated as if they had a value. See [Missing value](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_missing_value_5) in Elasticsearch's documentation for more information. +- **Missing** - Defines how documents missing a value should be treated. Missing values are ignored by default, but they can be treated as if they had a value. Refer to [Missing value](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_missing_value_5) in the Elasticsearch documentation for more information. Configure the following options for the **filters** bucket aggregation option: @@ -114,8 +121,8 @@ Configure the following options for the **geo hash grid** bucket aggregation opt Configure the following options for the **histogram** bucket aggregation option: -- **Interval** - Group by a type of interval. There are option to choose from the dropdown menu to select seconds, minutes, hours or day. You can also add a custom interval such as `30d` (30 days). `Auto` is the default option. -- **Min doc count** - The minimum amount of data to include in your query. The default is `0` +- **Interval** - The numeric interval for grouping values into buckets. +- **Min doc count** - The minimum number of documents required to include a bucket. The default is `0`. The **nested** group by option is currently experimental, you can select a field and then settings specific to that field. @@ -141,7 +148,7 @@ The option to run a **raw document query** is deprecated as of Grafana v10.1. ## Use template variables -You can also augment queries by using [template variables](../template-variables/). +You can also augment queries by using [template variables](https://grafana.com/docs/grafana//datasources/elasticsearch/template-variables/). Queries of `terms` have a 500-result limit by default. To set a custom limit, set the `size` property in your query. diff --git a/docs/sources/datasources/elasticsearch/template-variables/index.md b/docs/sources/datasources/elasticsearch/template-variables/index.md index 66ca17a93bd..ed2cb95f3af 100644 --- a/docs/sources/datasources/elasticsearch/template-variables/index.md +++ b/docs/sources/datasources/elasticsearch/template-variables/index.md @@ -22,6 +22,11 @@ refs: destination: /docs/grafana//dashboards/variables/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//dashboards/variables/ + add-template-variables-add-ad-hoc-filters: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/variables/add-template-variables/#add-ad-hoc-filters + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//dashboards/variables/add-template-variables/#add-ad-hoc-filters add-template-variables-multi-value-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/add-template-variables/#multi-value-variables @@ -37,11 +42,29 @@ refs: # Elasticsearch template variables Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. -Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard. +Grafana lists these variables in drop-down select boxes at the top of the dashboard to help you change the data displayed in your dashboard. Grafana refers to such variables as template variables. For an introduction to templating and template variables, refer to the [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables) documentation. +## Use ad hoc filters + +Elasticsearch supports the **Ad hoc filters** variable type. +You can use this variable type to specify any number of key/value filters, and Grafana applies them automatically to all of your Elasticsearch queries. + +Ad hoc filters support the following operators: + +| Operator | Description | +| -------- | ------------------------------------------------------------- | +| `=` | Equals. Adds `AND field:"value"` to the query. | +| `!=` | Not equals. Adds `AND -field:"value"` to the query. | +| `=~` | Matches regex. Adds `AND field:/value/` to the query. | +| `!~` | Does not match regex. Adds `AND -field:/value/` to the query. | +| `>` | Greater than. Adds `AND field:>value` to the query. | +| `<` | Less than. Adds `AND field:}} -To use an ascending sort (`asc`) with doc_count (a bottom-N list), set `order: "asc"`. However, Elasticsearch [discourages this](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-order) because sorting by ascending doc count can return inaccurate results. -{{< /admonition >}} - -To keep terms in the doc count order, set the variable's Sort dropdown to **Disabled**. -You can alternatively use other sorting criteria, such as **Alphabetical**, to re-sort them. +This example defines a variable named `$host` that only shows hosts matching the selected `$environment`: +```json +{ "find": "terms", "field": "hostname", "query": "environment:$environment" } ``` -{"find": "terms", "field": "hostname", "orderBy": "doc_count"} -``` + +Whenever you change the value of the `$environment` variable via the drop-down, Grafana triggers an update of the `$host` variable to contain only hostnames filtered by the selected environment. + +### Variables in aggregations + +You can use variables in bucket aggregation fields to dynamically change how data is grouped. For example, use a variable in the **Terms** group by field to let users switch between grouping by `hostname`, `service`, or `datacenter`. ## Template variable examples @@ -92,11 +116,36 @@ Write the query using a custom JSON string, with the field mapped as a [keyword] If the query is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, use `"field":"fieldname.keyword"` (sometimes `fieldname.raw`) to specify the keyword field in your query. -| Query | Description | -| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. | -| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. | -| `{"find": "terms", "field": "hostname", "query": ''}` | Returns a list of values for a keyword field using term aggregation and a specified Lucene query filter. Query will use current dashboard time range as time range for query. | +| Query | Description | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. | +| `{"find": "fields", "type": "number"}` | Returns a list of numeric field names (includes `float`, `double`, `integer`, `long`, `scaled_float`). | +| `{"find": "fields", "type": "date"}` | Returns a list of date field names. | +| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword field. Uses the current dashboard time range. | +| `{"find": "terms", "field": "hostname", "query": ""}` | Returns a list of values filtered by a Lucene query. Uses the current dashboard time range. | +| `{"find": "terms", "field": "status", "orderBy": "doc_count"}` | Returns values sorted by document count (descending by default). | +| `{"find": "terms", "field": "status", "orderBy": "doc_count", "order": "asc"}` | Returns values sorted by document count in ascending order. | -Queries of `terms` have a 500-result limit by default. -To set a custom limit, set the `size` property in your query. +Queries of `terms` have a 500-result limit by default. To set a custom limit, set the `size` property in your query. + +### Sort query results + +By default, queries return results in term order (which can then be sorted alphabetically or numerically using the variable's Sort setting). + +To produce a list of terms sorted by document count (a top-N values list), add an `orderBy` property of `doc_count`. This automatically selects a descending sort: + +```json +{ "find": "terms", "field": "status", "orderBy": "doc_count" } +``` + +You can also use the `order` property to explicitly set ascending or descending sort: + +```json +{ "find": "terms", "field": "hostname", "orderBy": "doc_count", "order": "asc" } +``` + +{{< admonition type="note" >}} +Elasticsearch [discourages](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-order) sorting by ascending doc count because it can return inaccurate results. +{{< /admonition >}} + +To keep terms in the document count order, set the variable's Sort drop-down to **Disabled**. You can alternatively use other sorting criteria, such as **Alphabetical**, to re-sort them. diff --git a/docs/sources/datasources/elasticsearch/troubleshooting/index.md b/docs/sources/datasources/elasticsearch/troubleshooting/index.md new file mode 100644 index 00000000000..ff0f73c6093 --- /dev/null +++ b/docs/sources/datasources/elasticsearch/troubleshooting/index.md @@ -0,0 +1,266 @@ +--- +aliases: + - ../../data-sources/elasticsearch/troubleshooting/ +description: Troubleshooting the Elasticsearch data source in Grafana +keywords: + - grafana + - elasticsearch + - troubleshooting + - errors +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Troubleshooting +title: Troubleshoot issues with the Elasticsearch data source +weight: 600 +--- + +# Troubleshoot issues with the Elasticsearch data source + +This document provides troubleshooting information for common errors you may encounter when using the Elasticsearch data source in Grafana. + +## Connection errors + +The following errors occur when Grafana cannot establish or maintain a connection to Elasticsearch. + +### Failed to connect to Elasticsearch + +**Error message:** "Health check failed: Failed to connect to Elasticsearch" + +**Cause:** Grafana cannot establish a network connection to the Elasticsearch server. + +**Solution:** + +1. Verify that the Elasticsearch URL is correct in the data source configuration. +1. Check that Elasticsearch is running and accessible from the Grafana server. +1. Ensure there are no firewall rules blocking the connection. +1. If using a proxy, verify the proxy settings are correct. +1. For Grafana Cloud, ensure you have configured [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) if your Elasticsearch instance is not publicly accessible. + +### Request timed out + +**Error message:** "Health check failed: Elasticsearch data source is not healthy. Request timed out" + +**Cause:** The connection to Elasticsearch timed out before receiving a response. + +**Solution:** + +1. Check the network latency between Grafana and Elasticsearch. +1. Verify that Elasticsearch is not overloaded or experiencing performance issues. +1. Increase the timeout setting in the data source configuration if needed. +1. Check if any network devices (load balancers, proxies) are timing out the connection. + +### Failed to parse data source URL + +**Error message:** "Failed to parse data source URL" + +**Cause:** The URL entered in the data source configuration is not valid. + +**Solution:** + +1. Verify the URL format is correct (for example, `http://localhost:9200` or `https://elasticsearch.example.com:9200`). +1. Ensure the URL includes the protocol (`http://` or `https://`). +1. Remove any trailing slashes or invalid characters from the URL. + +## Authentication errors + +The following errors occur when there are issues with authentication credentials or permissions. + +### Unauthorized (401) + +**Error message:** "Health check failed: Elasticsearch data source is not healthy. Status: 401 Unauthorized" + +**Cause:** The authentication credentials are invalid or missing. + +**Solution:** + +1. Verify that the username and password are correct. +1. If using an API key, ensure the key is valid and has not expired. +1. Check that the authentication method selected matches your Elasticsearch configuration. +1. Verify the user has the required permissions to access the Elasticsearch cluster. + +### Forbidden (403) + +**Error message:** "Health check failed: Elasticsearch data source is not healthy. Status: 403 Forbidden" + +**Cause:** The authenticated user does not have permission to access the requested resource. + +**Solution:** + +1. Verify the user has read access to the specified index. +1. Check Elasticsearch security settings and role mappings. +1. Ensure the user has permission to access the `_cluster/health` endpoint. +1. If using AWS Elasticsearch Service with SigV4 authentication, verify the IAM policy grants the required permissions. + +## Cluster health errors + +The following errors occur when the Elasticsearch cluster is unhealthy or unavailable. + +### Cluster status is red + +**Error message:** "Health check failed: Elasticsearch data source is not healthy" + +**Cause:** The Elasticsearch cluster health status is red, indicating one or more primary shards are not allocated. + +**Solution:** + +1. Check the Elasticsearch cluster health using `GET /_cluster/health`. +1. Review Elasticsearch logs for errors. +1. Verify all nodes in the cluster are running and connected. +1. Check for unassigned shards using `GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason`. +1. Consider increasing the cluster's resources or reducing the number of shards. + +### Bad Gateway (502) + +**Error message:** "Health check failed: Elasticsearch data source is not healthy. Status: 502 Bad Gateway" + +**Cause:** A proxy or load balancer between Grafana and Elasticsearch returned an error. + +**Solution:** + +1. Check the health of any proxies or load balancers in the connection path. +1. Verify Elasticsearch is running and accepting connections. +1. Review proxy/load balancer logs for more details. +1. Ensure the proxy timeout is configured appropriately for Elasticsearch requests. + +## Index errors + +The following errors occur when there are issues with the configured index or index pattern. + +### Index not found + +**Error message:** "Error validating index: index_not_found" + +**Cause:** The specified index or index pattern does not match any existing indices. + +**Solution:** + +1. Verify the index name or pattern in the data source configuration. +1. Check that the index exists using `GET /_cat/indices`. +1. If using a time-based index pattern (for example, `[logs-]YYYY.MM.DD`), ensure indices exist for the selected time range. +1. Verify the user has permission to access the index. + +### Time field not found + +**Error message:** "Could not find time field '@timestamp' with type date in index" + +**Cause:** The specified time field does not exist in the index or is not of type `date`. + +**Solution:** + +1. Verify the time field name in the data source configuration matches the field in your index. +1. Check the field mapping using `GET //_mapping`. +1. Ensure the time field is mapped as a `date` type, not `text` or `keyword`. +1. If the field name is different (for example, `timestamp` instead of `@timestamp`), update the data source configuration. + +## Query errors + +The following errors occur when there are issues with query syntax or configuration. + +### Too many buckets + +**Error message:** "Trying to create too many buckets. Must be less than or equal to: [65536]." + +**Cause:** The query is generating more aggregation buckets than Elasticsearch allows. + +**Solution:** + +1. Reduce the time range of your query. +1. Increase the date histogram interval (for example, change from `10s` to `1m`). +1. Add filters to reduce the number of documents being aggregated. +1. Increase the `search.max_buckets` setting in Elasticsearch (requires cluster admin access). + +### Required field missing + +**Error message:** "Required one of fields [field, script], but none were specified." + +**Cause:** A metric aggregation (such as Average, Sum, or Min) was added without specifying a field. + +**Solution:** + +1. Select a field for the metric aggregation in the query editor. +1. Ensure the selected field exists in your index and contains numeric data. + +### Unsupported interval + +**Error message:** "unsupported interval '<interval>'" + +**Cause:** The interval specified for the index pattern is not valid. + +**Solution:** + +1. Use a supported interval: `Hourly`, `Daily`, `Weekly`, `Monthly`, or `Yearly`. +1. If you don't need a time-based index pattern, use `No pattern` and specify the exact index name. + +## Version errors + +The following errors occur when there are Elasticsearch version compatibility issues. + +### Unsupported Elasticsearch version + +**Error message:** "Support for Elasticsearch versions after their end-of-life (currently versions < 7.16) was removed. Using unsupported version of Elasticsearch may lead to unexpected and incorrect results." + +**Cause:** The Elasticsearch version is no longer supported by the Grafana data source. + +**Solution:** + +1. Upgrade Elasticsearch to a supported version (7.17+, 8.x, or 9.x). +1. Refer to [Elastic Product End of Life Dates](https://www.elastic.co/support/eol) for version support information. +1. Note that queries may still work, but Grafana does not guarantee functionality for unsupported versions. + +## Other common issues + +The following issues don't produce specific error messages but are commonly encountered. + +### Empty query results + +**Cause:** The query returns no data. + +**Solution:** + +1. Verify the time range includes data in your index. +1. Check the Lucene query syntax for errors. +1. Test the query directly in Elasticsearch using the `_search` API. +1. Ensure the index contains documents matching your query filters. + +### Slow query performance + +**Cause:** Queries take a long time to execute. + +**Solution:** + +1. Reduce the time range of your query. +1. Add more specific filters to limit the data scanned. +1. Increase the date histogram interval. +1. Check Elasticsearch cluster performance and resource utilization. +1. Consider using index aliases or data streams for better query routing. + +### CORS errors in browser console + +**Cause:** Cross-Origin Resource Sharing (CORS) is blocking requests from the browser to Elasticsearch. + +**Solution:** + +1. Use Server (proxy) access mode instead of Browser access mode in the data source configuration. +1. If Browser access is required, configure CORS settings in Elasticsearch: + +```yaml +http.cors.enabled: true +http.cors.allow-origin: '' +http.cors.allow-headers: 'Authorization, Content-Type' +http.cors.allow-credentials: true +``` + +{{< admonition type="note" >}} +Server (proxy) access mode is recommended for security and reliability. +{{< /admonition >}} + +## Get additional help + +If you continue to experience issues after following this troubleshooting guide: + +1. Check the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) for API-specific guidance. +1. Review the [Grafana community forums](https://community.grafana.com/) for similar issues. +1. Contact Grafana Support if you have an Enterprise license. diff --git a/docs/sources/datasources/influxdb/_index.md b/docs/sources/datasources/influxdb/_index.md index c3f722d848f..4ede6c02c52 100644 --- a/docs/sources/datasources/influxdb/_index.md +++ b/docs/sources/datasources/influxdb/_index.md @@ -52,6 +52,7 @@ The following documents will help you get started with the InfluxDB data source - [Configure the InfluxDB data source](./configure-influxdb-data-source/) - [InfluxDB query editor](./query-editor/) - [InfluxDB templates and variables](./template-variables/) +- [Troubleshoot issues with the InfluxDB data source](./troubleshooting/) Once you have configured the data source you can: diff --git a/docs/sources/datasources/influxdb/troubleshooting/index.md b/docs/sources/datasources/influxdb/troubleshooting/index.md new file mode 100644 index 00000000000..33fe67ecadf --- /dev/null +++ b/docs/sources/datasources/influxdb/troubleshooting/index.md @@ -0,0 +1,291 @@ +--- +aliases: + - ../../data-sources/influxdb/troubleshooting/ +description: Troubleshooting the InfluxDB data source in Grafana +keywords: + - grafana + - influxdb + - troubleshooting + - errors + - flux + - influxql + - sql +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Troubleshooting +title: Troubleshoot issues with the InfluxDB data source +weight: 600 +--- + +# Troubleshoot issues with the InfluxDB data source + +This document provides troubleshooting information for common errors you may encounter when using the InfluxDB data source in Grafana. + +## Connection errors + +The following errors occur when Grafana cannot establish or maintain a connection to InfluxDB. + +### Failed to connect to InfluxDB + +**Error message:** "error performing influxQL query" or "error performing flux query" or "error performing sql query" + +**Cause:** Grafana cannot establish a network connection to the InfluxDB server. + +**Solution:** + +1. Verify that the InfluxDB URL is correct in the data source configuration. +1. Check that InfluxDB is running and accessible from the Grafana server. +1. Ensure the URL includes the protocol (`http://` or `https://`). +1. Verify the port is correct (the InfluxDB default API port is `8086`). +1. Ensure there are no firewall rules blocking the connection. +1. For Grafana Cloud, ensure you have configured [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) if your InfluxDB instance is not publicly accessible. + +### Request timed out + +**Error message:** "context deadline exceeded" or "request timeout" + +**Cause:** The connection to InfluxDB timed out before receiving a response. + +**Solution:** + +1. Check the network latency between Grafana and InfluxDB. +1. Verify that InfluxDB is not overloaded or experiencing performance issues. +1. Increase the timeout setting in the data source configuration under **Advanced HTTP Settings**. +1. Reduce the time range or complexity of your query. + +## Authentication errors + +The following errors occur when there are issues with authentication credentials or permissions. + +### Unauthorized (401) + +**Error message:** "401 Unauthorized" or "authorization failed" + +**Cause:** The authentication credentials are invalid or missing. + +**Solution:** + +1. Verify that the token or password is correct in the data source configuration. +1. For Flux and SQL, ensure the token has not expired. +1. For InfluxQL with InfluxDB 2.x, verify the token is set as an `Authorization` header with the value `Token `. +1. For InfluxDB 1.x, verify the username and password are correct. +1. Check that the token has the required permissions to access the specified bucket or database. + +### Forbidden (403) + +**Error message:** "403 Forbidden" or "access denied" + +**Cause:** The authenticated user or token does not have permission to access the requested resource. + +**Solution:** + +1. Verify the token has read access to the specified bucket or database. +1. Check the token's permissions in the InfluxDB UI under **API Tokens**. +1. Ensure the organization ID is correct for Flux queries. +1. For InfluxQL with InfluxDB 2.x, verify the DBRP mapping is configured correctly. + +## Configuration errors + +The following errors occur when the data source is not configured correctly. + +### Unknown influx version + +**Error message:** "unknown influx version" + +**Cause:** The query language is not properly configured in the data source settings. + +**Solution:** + +1. Open the data source configuration in Grafana. +1. Verify that a valid query language is selected: **Flux**, **InfluxQL**, or **SQL**. +1. Ensure the selected query language matches your InfluxDB version: + - Flux: InfluxDB 1.8+ and 2.x + - InfluxQL: InfluxDB 1.x and 2.x (with DBRP mapping) + - SQL: InfluxDB 3.x only + +### Invalid data source info received + +**Error message:** "invalid data source info received" + +**Cause:** The data source configuration is incomplete or corrupted. + +**Solution:** + +1. Delete and recreate the data source. +1. Ensure all required fields are populated based on your query language: + - **Flux:** URL, Organization, Token, Default Bucket + - **InfluxQL:** URL, Database, User, Password + - **SQL:** URL, Database, Token + +### DBRP mapping required + +**Error message:** "database not found" or queries return no data with InfluxQL on InfluxDB 2.x + +**Cause:** InfluxQL queries on InfluxDB 2.x require a Database and Retention Policy (DBRP) mapping. + +**Solution:** + +1. Create a DBRP mapping in InfluxDB using the CLI or API. +1. Refer to [Manage DBRP Mappings](https://docs.influxdata.com/influxdb/cloud/query-data/influxql/dbrp/) for guidance. +1. Verify the database name in Grafana matches the DBRP mapping. + +## Query errors + +The following errors occur when there are issues with query syntax or execution. + +### Query syntax error + +**Error message:** "error parsing query: found THING" or "failed to parse query: found WERE, expected ; at line 1, char 38" + +**Cause:** The query contains invalid syntax. + +**Solution:** + +1. Check your query syntax for typos or invalid keywords. +1. For InfluxQL, verify the query follows the correct syntax: + + ```sql + SELECT FROM WHERE + ``` + +1. For Flux, ensure proper pipe-forward syntax and function calls. +1. Use the InfluxDB UI or CLI to test your query directly. + +### Query timeout limit exceeded + +**Error message:** "query-timeout limit exceeded" + +**Cause:** The query took longer than the configured timeout limit in InfluxDB. + +**Solution:** + +1. Reduce the time range of your query. +1. Add more specific filters to limit the data scanned. +1. Increase the query timeout setting in InfluxDB if you have admin access. +1. Optimize your query to reduce complexity. + +### Too many series or data points + +**Error message:** "max-series-per-database limit exceeded" or "A query returned too many data points and the results have been truncated" + +**Cause:** The query is returning more data than the configured limits allow. + +**Solution:** + +1. Reduce the time range of your query. +1. Add filters to limit the number of series returned. +1. Increase the **Max series** setting in the data source configuration under **Advanced Database Settings**. +1. Use aggregation functions to reduce the number of data points. +1. For Flux, use `aggregateWindow()` to downsample data. + +### No time column found + +**Error message:** "no time column found" + +**Cause:** The query result does not include a time column, which is required for time series visualization. + +**Solution:** + +1. Ensure your query includes a time field. +1. For Flux, verify the query includes `_time` in the output. +1. For SQL, ensure the query returns a timestamp column. +1. Check that the time field is not being filtered out or excluded. + +## Health check errors + +The following errors occur when testing the data source connection. + +### Error getting flux query buckets + +**Error message:** "error getting flux query buckets" + +**Cause:** The health check query `buckets()` failed to return results. + +**Solution:** + +1. Verify the token has permission to list buckets. +1. Check that the organization ID is correct. +1. Ensure InfluxDB is running and accessible. + +### Error connecting InfluxDB influxQL + +**Error message:** "error connecting InfluxDB influxQL" + +**Cause:** The health check query `SHOW MEASUREMENTS` failed. + +**Solution:** + +1. Verify the database name is correct. +1. Check that the user has permission to run `SHOW MEASUREMENTS`. +1. Ensure the database exists and contains measurements. +1. For InfluxDB 2.x, verify DBRP mapping is configured. + +### 0 measurements found + +**Error message:** "data source is working. 0 measurements found" + +**Cause:** The connection is successful, but the database contains no measurements. + +**Solution:** + +1. Verify you are connecting to the correct database. +1. Check that data has been written to the database. +1. If the database is new, add some test data to verify the connection. + +## Other common issues + +The following issues don't produce specific error messages but are commonly encountered. + +### Empty query results + +**Cause:** The query returns no data. + +**Solution:** + +1. Verify the time range includes data in your database. +1. Check that the measurement and field names are correct. +1. Test the query directly in the InfluxDB UI or CLI. +1. Ensure filters are not excluding all data. +1. For InfluxQL, verify the retention policy contains data for the selected time range. + +### Slow query performance + +**Cause:** Queries take a long time to execute. + +**Solution:** + +1. Reduce the time range of your query. +1. Add more specific filters to limit the data scanned. +1. Increase the **Min time interval** setting to reduce the number of data points. +1. Check InfluxDB server performance and resource utilization. +1. For Flux, use `aggregateWindow()` to downsample data before visualization. +1. Consider using continuous queries or tasks to pre-aggregate data. + +### Data appears delayed or missing recent points + +**Cause:** The visualization doesn't show the most recent data. + +**Solution:** + +1. Check the dashboard time range and refresh settings. +1. Verify the **Min time interval** is not set too high. +1. Ensure InfluxDB has finished writing the data. +1. Check for clock synchronization issues between Grafana and InfluxDB. + +## Get additional help + +If you continue to experience issues after following this troubleshooting guide: + +1. Check the [InfluxDB documentation](https://docs.influxdata.com/) for API-specific guidance. +1. Review the [Grafana community forums](https://community.grafana.com/) for similar issues. +1. Contact Grafana Support if you're an Enterprise, Cloud Pro or Cloud Contracted user. +1. When reporting issues, include: + - Grafana version + - InfluxDB version and product (OSS, Cloud, Enterprise) + - Query language (Flux, InfluxQL, or SQL) + - Error messages (redact sensitive information) + - Steps to reproduce + - Relevant configuration such as data source settings, HTTP method, and TLS settings (redact tokens, passwords, and other credentials) diff --git a/docs/sources/datasources/mysql/troubleshoot/index.md b/docs/sources/datasources/mysql/troubleshoot/index.md new file mode 100644 index 00000000000..0fcf80e55ac --- /dev/null +++ b/docs/sources/datasources/mysql/troubleshoot/index.md @@ -0,0 +1,80 @@ +--- +description: Learn how to troubleshoot common problems with the Grafana MySQL data source plugin +keywords: + - grafana + - mysql + - query +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Troubleshoot +title: Troubleshoot common problems with the Grafana MySQL data source plugin +weight: 40 +refs: + variables: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/variables/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/ + variable-syntax-advanced-variable-format-options: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/variables/variable-syntax/#advanced-variable-format-options + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#advanced-variable-format-options + annotate-visualizations: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/ + explore: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//explore/ + query-transform-data: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/query-transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ + panel-inspector: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/panel-inspector/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-inspector/ + query-editor: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/query-transform-data/#query-editors + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#query-editors + alert-rules: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/ + template-annotations-and-labels: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/templates/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/ + configure-standard-options: + - pattern: /docs/grafana/ + - destination: /docs/grafana//panels-visualizations/configure-standard-options/ +--- + +# Troubleshoot common problems with the Grafana MySQL data source plugin + +This page lists common issues you might experience when setting up the Grafana MySQL data source plugin. + +### My data source connection fails when using the Grafana MySQL data source plugin + +- Check if the MySQL server is up and running. +- Make sure that your firewall is open for MySQL server (default port is `3306`). +- Ensure that you have the correct permissions to access the MySQL server and also have permission to access the database. +- If the error persists, create a new user for the Grafana MySQL data source plugin with correct permissions and try to connect with it. + +### What should I do if I see "An unexpected error happened" or "Could not connect to MySQL" after trying all of the above? + +- Check the Grafana logs for more details about the error. +- For Grafana Cloud customers, contact support. diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 1898cd46b94..cae948ce4f3 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -369,7 +369,7 @@ "description": "For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/developers/plugin-tools/how-to-guides/data-source-plugins/add-authentication-for-data-source-plugins).", "items": { "type": "object", - "description": "", + "description": "For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/developers/plugin-tools/how-to-guides/data-source-plugins/add-authentication-for-data-source-plugins).", "additionalProperties": false, "properties": { "path": { diff --git a/docs/sources/fundamentals/glossary/index.md b/docs/sources/fundamentals/glossary/index.md index eee83a7f2de..428764b4ae5 100644 --- a/docs/sources/fundamentals/glossary/index.md +++ b/docs/sources/fundamentals/glossary/index.md @@ -83,6 +83,11 @@ This topic lists words and abbreviations that are commonly used in the Grafana d A commonly-used visualization that displays data as points, lines, or bars. + + grafanactl + + A command-line tool that enables users to authenticate, manage multiple environments, and perform administrative tasks through Grafana's REST API. + mixin diff --git a/docs/sources/setup-grafana/configure-access/configure-scim-provisioning/_index.md b/docs/sources/setup-grafana/configure-access/configure-scim-provisioning/_index.md index 9ef98bbc433..a07dde69e08 100644 --- a/docs/sources/setup-grafana/configure-access/configure-scim-provisioning/_index.md +++ b/docs/sources/setup-grafana/configure-access/configure-scim-provisioning/_index.md @@ -54,18 +54,6 @@ SCIM offers several advantages for managing users and teams in Grafana: ## Authentication and access requirements -{{< admonition type="warning" title="Critical: Aligning SAML Identifier with SCIM externalId" >}} -When using SAML for authentication alongside SCIM provisioning, a critical security measure is to ensure proper alignment between the the SCIM user's `externalId` and the SAML user identifier. The unique identifier used for SCIM provisioning (which becomes the `externalId` in Grafana, often sourced from a stable IdP attribute like Entra ID's `user.objectid`) **must also be sent as a claim in the SAML assertion from your Identity Provider.** -Furthermore, the Grafana SAML configuration must be correctly set up to identify and use this specific claim for linking the authenticated SAML user to their SCIM-provisioned user. This can be achieved by either ensuring the primary SAML login identifier by using the `assertion_attribute_external_uid` setting in Grafana to explicitly set the name of the SAML claim that contains the stable unique identifier attribute. - -**Why is this important?** -A mismatch or inconsistent mapping between this SAML login identifier and the SCIM `externalId` creates a critical security vulnerability. If these two identifiers are not reliably and uniquely aligned for each individual user, Grafana may fail to correctly link an authenticated SAML session to the intended SCIM-provisioned user profile and its associated permissions. This can enable a malicious actor to impersonate another user—for instance, by crafting a SAML assertion that, due to the identifier misalignment, incorrectly grants them the access rights of the targeted user. - -Grafana relies on this linkage to correctly associate the authenticated user from SAML with the provisioned user from SCIM. Failure to ensure a consistent and unique identifier across both systems can break this linkage, leading to incorrect user mapping and potential unauthorized access. - -Always verify that your SAML identity provider is configured to send a stable, unique user identifier that your SCIM configuration maps to `externalId`. Refer to your identity provider's documentation and the specific Grafana SCIM integration guides (e.g., for [Entra ID](configure-scim-with-azuread/) or [Okta](configure-scim-with-okta/)) for detailed instructions on configuring these attributes correctly. -{{< /admonition >}} - When you enable SCIM in Grafana, the following requirements and restrictions apply: 1. **Use the same identity provider for user provisioning and for authentication flow**: You must use the same identity provider for both authentication and user provisioning. @@ -74,6 +62,12 @@ When you enable SCIM in Grafana, the following requirements and restrictions app - Configure `userUID` SAML assertion in [Entra ID](/docs/grafana//setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-azuread/#configure-saml-assertions-when-using-scim-provisioning) - Configure `userUID` SAML assertion in [Okta](/docs/grafana//setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-okta/#configure-saml-assertions-when-using-scim-provisioning) +### Align SAML identifier with SCIM `externalId` + +When you use SAML with SCIM provisioning, align the SCIM `externalId` with the SAML user identifier. Use a stable IdP attribute (for example, Entra ID `user.objectid`) as the SCIM `externalId`, and send that same value as a SAML claim. Configure Grafana to read this claim with the `assertion_attribute_external_uid` setting so SAML authentication links to the SCIM-provisioned user and its permissions. + +If the SAML identifier and SCIM `externalId` differ, Grafana may not link the authenticated user to the intended SCIM profile, which can result in incorrect access. Verify your IdP sends a stable, unique identifier and that it matches the SCIM `externalId`. Refer to your IdP docs and the Grafana SCIM integration guides for [Entra ID](configure-scim-with-azuread/) and [Okta](configure-scim-with-okta/) for attribute configuration details. + ## Configure SCIM using the Grafana user interface You can configure SCIM in Grafana using the Grafana user interface. To do this, navigate to **Administration > Authentication > SCIM**. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 13c2b77fd2f..67c361b2bdc 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -642,6 +642,12 @@ You must also provide the `rudderstack_write_key` to enable this feature. Optional. If tracking with RudderStack is enabled, you can provide a custom URL to load the RudderStack SDK. +#### `rudderstack_v3_sdk_url` + +Optional. +This is mirroring the old configuration option, which will be deprecated. +If `rudderstack_sdk_url` and `rudderstack_v3_sdk_url` are both set, the feature toggle `rudderstackUpgrade` will control which one is loaded. + #### `rudderstack_config_url` Optional. @@ -1776,6 +1782,13 @@ Specify the frequency of polling for Alertmanager configuration changes. The def The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), for example, 30s or 1m. +#### `alertmanager_max_template_output_bytes` + +Maximum size in bytes that the expanded result of any single template expression (e.g. {{ .CommonAnnotations.description }}, {{ .ExternalURL }}, etc.) may reach during notification rendering. +The limit is checked after template execution for each templated field, but before the value is inserted into the final notification payload sent to the receiver. +If exceeded, the notification will contain output truncated up to the limit and a warning will be logged. +The default value is 10,485,760 bytes (10Mb). + #### `ha_redis_address` Redis server address or addresses. It can be a single Redis address if using Redis standalone, @@ -2039,6 +2052,10 @@ This section applies only to rules imported as Grafana-managed rules. For more i Set the query offset to imported Grafana-managed rules when `query_offset` is not defined in the original rule group configuration. The default value is `1m`. +#### `default_datasource_uid` + +Set the default data source UID to use for query execution when importing Prometheus rules. Grafana uses this default when the `X-Grafana-Alerting-Datasource-UID` header isn't provided during import. If this option isn't set, the header becomes required. The default value is empty. +
### `[annotations]` @@ -2142,17 +2159,13 @@ Configures settings around the short link feature. #### `expire_time` -Short links that are never accessed are considered expired or stale and are deleted as cleanup. +Short links that are never accessed are considered expired or stale and can be deleted as cleanup. Set the expiration time in days. -The default is `7` days. +The default is `-1` days (never expire). The maximum is `365` days. -A setting above the maximum uses the value `365` instead. -Setting `0` means the short links are cleaned up approximately every 10 minutes. -A negative value such as `-1` disables expiry. -{{< admonition type="caution" >}} -Short links without an expiration increase the size of the database and can't be deleted. Grafana recommends setting a duration based on your specific use case -{{< /admonition >}} +A setting above the maximum uses the value `365` instead. +A negative value such as `-1` disables expiry.
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 b80fa49f815..16c97f6d10a 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -31,7 +31,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | | `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | -| `unifiedRequestLog` | Writes error logs to the request logger | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | diff --git a/docs/sources/setup-grafana/installation/debian/index.md b/docs/sources/setup-grafana/installation/debian/index.md index 067bcb29f6a..900c4a4b9d1 100644 --- a/docs/sources/setup-grafana/installation/debian/index.md +++ b/docs/sources/setup-grafana/installation/debian/index.md @@ -46,7 +46,7 @@ Complete the following steps to install Grafana from the APT repository: 1. Install the prerequisite packages: ```bash - sudo apt-get install -y apt-transport-https software-properties-common wget + sudo apt-get install -y apt-transport-https wget ``` 1. Import the GPG key: diff --git a/docs/sources/upgrade-guide/upgrade-v12.0/index.md b/docs/sources/upgrade-guide/upgrade-v12.0/index.md index 779950c0fd4..b166cc2c022 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.0/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.0/index.md @@ -80,3 +80,87 @@ Since Grafana 10.2, the endpoint to check compatible versions when installing a #### What if I want to ignore the compatibility check? We _do not_ recommend installing plugins declared as incompatible. However, if you need to force install a plugin despite it being declared as incompatible, refer to the [Installing a plugin from a ZIP](https://grafana.com/docs/grafana/latest/administration/plugin-management/#install-a-plugin-from-a-zip-file) guidance. + +### Annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the `annotation` table. The migration populates the new `dashboard_uid` column, which causes the database to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the database data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your database. + +To check your annotation table size, connect to your database and check the table size. + +For PostgreSQL, run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +For MySQL, run the following query: + +```sql +SELECT + ROUND(data_length / 1024 / 1024, 2) AS table_size_mb, + ROUND(index_length / 1024 / 1024, 2) AS indexes_size_mb, + ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'annotation'; +``` + +For SQLite, check the database file size directly, as SQLite stores all tables in a single file. You can run the following command from your terminal: + +```bash +ls -lh +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your database data volume. + +1. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +1. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +1. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by performing database maintenance operations during a maintenance window. + +For PostgreSQL, run a `VACUUM FULL` operation on the `annotation` table: + +```sql +VACUUM FULL annotation; +``` + +For MySQL, run an `OPTIMIZE TABLE` operation on the `annotation` table: + +```sql +OPTIMIZE TABLE annotation; +``` + +For SQLite, run a `VACUUM` operation on the database: + +```sql +VACUUM; +``` + +These operations require a lock on the table and may take significant time depending on the table size. Plan to run these during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.1/index.md b/docs/sources/upgrade-guide/upgrade-v12.1/index.md index 74095298c90..9a96c0cbc0a 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.1/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.1/index.md @@ -20,3 +20,87 @@ weight: 499 {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} ## Technical notes + +### Annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the `annotation` table. The migration populates the new `dashboard_uid` column, which causes the database to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the database data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your database. + +To check your annotation table size, connect to your database and check the table size. + +For PostgreSQL, run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +For MySQL, run the following query: + +```sql +SELECT + ROUND(data_length / 1024 / 1024, 2) AS table_size_mb, + ROUND(index_length / 1024 / 1024, 2) AS indexes_size_mb, + ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'annotation'; +``` + +For SQLite, check the database file size directly, as SQLite stores all tables in a single file. You can run the following command from your terminal: + +```bash +ls -lh +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your database data volume. + +1. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +1. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +1. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by performing database maintenance operations during a maintenance window. + +For PostgreSQL, run a `VACUUM FULL` operation on the `annotation` table: + +```sql +VACUUM FULL annotation; +``` + +For MySQL, run an `OPTIMIZE TABLE` operation on the `annotation` table: + +```sql +OPTIMIZE TABLE annotation; +``` + +For SQLite, run a `VACUUM` operation on the database: + +```sql +VACUUM; +``` + +These operations require a lock on the table and may take significant time depending on the table size. Plan to run these during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.2/index.md b/docs/sources/upgrade-guide/upgrade-v12.2/index.md index d2605fc51d1..221ae3f152c 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.2/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.2/index.md @@ -18,3 +18,89 @@ weight: 498 {{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes + +### Annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the `annotation` table. The migration populates the new `dashboard_uid` column, which causes the database to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the database data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your database. + +To check your annotation table size, connect to your database and check the table size. + +For PostgreSQL, run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +For MySQL, run the following query: + +```sql +SELECT + ROUND(data_length / 1024 / 1024, 2) AS table_size_mb, + ROUND(index_length / 1024 / 1024, 2) AS indexes_size_mb, + ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'annotation'; +``` + +For SQLite, check the database file size directly, as SQLite stores all tables in a single file. You can run the following command from your terminal: + +```bash +ls -lh +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your database data volume. + +1. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +1. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +1. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by performing database maintenance operations during a maintenance window. + +For PostgreSQL, run a `VACUUM FULL` operation on the `annotation` table: + +```sql +VACUUM FULL annotation; +``` + +For MySQL, run an `OPTIMIZE TABLE` operation on the `annotation` table: + +```sql +OPTIMIZE TABLE annotation; +``` + +For SQLite, run a `VACUUM` operation on the database: + +```sql +VACUUM; +``` + +These operations require a lock on the table and may take significant time depending on the table size. Plan to run these during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.3/index.md b/docs/sources/upgrade-guide/upgrade-v12.3/index.md index 8d3dacbf396..6160c443e10 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.3/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.3/index.md @@ -18,3 +18,89 @@ weight: 497 {{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes + +### Annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the `annotation` table. The migration populates the new `dashboard_uid` column, which causes the database to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the database data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your database. + +To check your annotation table size, connect to your database and check the table size. + +For PostgreSQL, run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +For MySQL, run the following query: + +```sql +SELECT + ROUND(data_length / 1024 / 1024, 2) AS table_size_mb, + ROUND(index_length / 1024 / 1024, 2) AS indexes_size_mb, + ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name = 'annotation'; +``` + +For SQLite, check the database file size directly, as SQLite stores all tables in a single file. You can run the following command from your terminal: + +```bash +ls -lh +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your database data volume. + +1. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +1. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +1. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by performing database maintenance operations during a maintenance window. + +For PostgreSQL, run a `VACUUM FULL` operation on the `annotation` table: + +```sql +VACUUM FULL annotation; +``` + +For MySQL, run an `OPTIMIZE TABLE` operation on the `annotation` table: + +```sql +OPTIMIZE TABLE annotation; +``` + +For SQLite, run a `VACUUM` operation on the database: + +```sql +VACUUM; +``` + +These operations require a lock on the table and may take significant time depending on the table size. Plan to run these during a low-traffic period. diff --git a/docs/sources/upgrade-guide/when-to-upgrade/index.md b/docs/sources/upgrade-guide/when-to-upgrade/index.md index e7a29e531e0..53d8c23330e 100644 --- a/docs/sources/upgrade-guide/when-to-upgrade/index.md +++ b/docs/sources/upgrade-guide/when-to-upgrade/index.md @@ -107,8 +107,8 @@ Here is an overview of version support through 2026: | 12.0.x | May 5, 2025 | February 5, 2026 | Patch Support | | 12.1.x | July 22, 2025 | April 22, 2026 | Patch Support | | 12.2.x | September 23, 2025 | June 23, 2026 | Patch Support | -| 12.3.x | November 18, 2025 | August 18, 2026 | Yet to be released | -| 12.4.x (Last minor of 12) | February 24, 2026 | November 24, 2026 | Yet to be released | +| 12.3.x | November 19, 2025 | August 19, 2026 | Patch Support | +| 12.4.x (Last minor of 12) | February 24, 2026 | May 24, 2027 | Yet to be released | | 13.0.0 | TBD | TBD | Yet to be released | ## How are these versions supported? 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 fcccfa6bd1b..5458f90281e 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md @@ -149,7 +149,10 @@ To add a new annotation query to a dashboard, follow these steps: You can also click **Open advanced data source picker** to see more options, including adding a data source (Admins only). 1. If you don't want to use the annotation query right away, clear the **Enabled** checkbox. -1. If you don't want the annotation query toggle to be displayed in the dashboard, select the **Hidden** checkbox. +1. Select one of the following options in the **Show annotation controls in** drop-down list to control where annotations are displayed: + - **Above dashboard** - The annotation toggle is displayed above the dashboard. This is the default. + - **Controls menu** - The annotation toggle is displayed in the dashboard controls menu instead of above the dashboard. The dashboard controls menu appears as a button in the dashboard toolbar. + - **Hidden** - The annotation toggle is not displayed on the dashboard. 1. Select a color for the event markers. 1. In the **Show in** drop-down, choose one of the following options: - **All panels** - The annotations are displayed on all panels that support annotations. @@ -160,9 +163,9 @@ To add a new annotation query to a dashboard, follow these steps: 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). + - Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries). -1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). +1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option. 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. 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 feb6573c3e9..10196a40811 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md @@ -99,7 +99,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee - Understand the query language of the target data source. - Ensure that data source for which you are writing a query has been added. For more information about adding a data source, refer to [Add a data source](ref:add-a-data-source) if you need instructions. -**To create a dashboard**: +To create a dashboard, follow these steps: {{< shared id="create-dashboard" >}} @@ -125,9 +125,9 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee 1. To create a query, do one of the following: - Write or construct a query in the query language of your data source. - - Click **Replace with saved query** to reuse a [saved query](ref:saved-queries). + - Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries). -1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). +1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option. 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. @@ -171,6 +171,28 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee Now, when you want to make more changes to the saved dashboard, click **Edit** in the top-right corner. +### Begin dashboard creation from data source configuration + +You can start the process of creating a dashboard directly from a data source rather than from the **Dashboards** page. + +To begin building a dashboard directly from a data source, follow these steps: + +1. Navigate to **Connections > Data sources**. +1. On the row of the data source for which you want to build a dashboard, click **Build a dashboard**. + + The empty dashboard page opens. + +1. Do one of the following: + - Click **+Add visualization** to configure all the elements of the new dashboard. + - Select one of the suggested dashboards by clicking its **Use dashboard** button. This can be helpful when you're not sure how to most effectively visualize your data. + The suggested dashboards are specific to your data source type (for example, Prometheus, Loki, or Elasticsearch). If there are more than three dashboard suggestions, you can click **View all** to see the rest of them. + + ![Empty dashboard with add visualization and suggested dashboard options](/media/docs/grafana/dashboards/screenshot-suggested-dashboards-v12.3.png) + + {{< docs/public-preview product="Suggested dashboards" >}} + +1. Complete the rest of the dashboard configuration. For more detailed steps, refer to [Create a dashboard](#create-a-dashboard), beginning at step five. + ## Copy a dashboard To copy a dashboard, follow these steps: diff --git a/docs/sources/visualizations/dashboards/build-dashboards/create-dynamic-dashboard/index.md b/docs/sources/visualizations/dashboards/build-dashboards/create-dynamic-dashboard/index.md index a77e4ca988f..0167ff147e5 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/create-dynamic-dashboard/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/create-dynamic-dashboard/index.md @@ -245,11 +245,12 @@ To configure repeats, follow these steps: 1. Click **Save**. 1. Toggle off the edit mode switch. -### Repeating rows and the Dashboard special data source +### Repeating rows and tabs and the Dashboard special data source If a row includes panels using the special [Dashboard data source](ref:built-in-special-data-sources)—the data source that uses a result set from another panel in the same dashboard—then corresponding panels in repeated rows will reference the panel in the original row, not the ones in the repeated rows. +The same behavior applies to tabs. For example, in a dashboard: diff --git a/docs/sources/visualizations/dashboards/build-dashboards/manage-dashboard-links/index.md b/docs/sources/visualizations/dashboards/build-dashboards/manage-dashboard-links/index.md index cddf9a5b9f5..3e94ec5aa22 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/manage-dashboard-links/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/manage-dashboard-links/index.md @@ -99,6 +99,7 @@ Add links to other dashboards at the top of your current dashboard. - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. For more information, see [Dashboard URL variables](ref:dashboard-url-variables). - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + - **Show in controls menu** – Select this option to display the link in the dashboard controls menu instead of at the top of the dashboard. The dashboard controls menu appears as a button in the dashboard toolbar. 1. Click **Save dashboard** in the top-right corner. 1. Click **Back to dashboard** and then **Exit edit**. @@ -121,6 +122,7 @@ Add a link to a URL at the top of your current dashboard. You can link to any av - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + - **Show in controls menu** – Select this option to display the link in the dashboard controls menu instead of at the top of the dashboard. The dashboard controls menu appears as a button in the dashboard toolbar. 1. Click **Save dashboard** in the top-right corner. 1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md b/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md index a2867759119..e7749ba5b88 100644 --- a/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md +++ b/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md @@ -98,7 +98,7 @@ You can share dashboards in the following ways: - [As a report](#schedule-a-report) - [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 a JSON file export](#export-a-dashboard-as-code) - [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**. @@ -223,17 +223,25 @@ To export a dashboard in its current state as a PDF, follow these steps: 1. Click the **X** at the top-right corner to close the share drawer. -### Export a dashboard as JSON +### Export a dashboard as code Export a Grafana JSON file that contains everything you need, including layout, variables, styles, data sources, queries, and so on, so that you can later import the dashboard. To export a JSON 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 JSON**. +1. Click the **Export** drop-down list in the top-right corner and select **Export as code**. - The **Export dashboard JSON** drawer opens. + The **Export dashboard** drawer opens. + +1. Select the dashboard JSON model that you to export: + - **Classic** - Export dashboards created using the [current dashboard schema](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/view-dashboard-json-model/). + - **V1 Resource** - Export dashboards created using the [current dashboard schema](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/view-dashboard-json-model/) wrapped in the `spec` property of the [V1 Kubernetes-style resource](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2alpha1). Choose between **JSON** and **YAML** format. + - **V2 Resource** - Export dashboards created using the [V2 Resource schema](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2beta1). Choose between **JSON** and **YAML** format. + +1. Do one of the following: + - Toggle the **Export for sharing externally** switch to generate the JSON with a different data source UID. + - Toggle the **Remove deployment details** switch to make the dashboard externally shareable. -1. Toggle the **Export the dashboard to use in another instance** switch to generate the JSON with a different data source UID. 1. Click **Download file** or **Copy to clipboard**. 1. Click the **X** at the top-right corner to close the share drawer. 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 69dbf4a2547..5ee61394a15 100644 --- a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md @@ -123,10 +123,11 @@ To create a variable, follow these steps: If you don't enter a display name, then the drop-down list label is the variable name. -1. Choose a **Show on dashboard** option: - - **Label and value** - The variable drop-down list displays the variable **Name** or **Label** value. This is the default. - - **Value:** The variable drop-down list only displays the selected variable value and a down arrow. - - **Nothing:** No variable drop-down list is displayed on the dashboard. +1. Choose a **Display** option: + - **Above dashboard** - The variable drop-down list displays above the dashboard with the variable **Name** or **Label** value. This is the default. + - **Above dashboard, label hidden** - The variable drop-down list displays above the dashboard, but without showing the name of the variable. + - **Controls menu** - The variable is displayed in the dashboard controls menu instead of above the dashboard. The dashboard controls menu appears as a button in the dashboard toolbar. + - **Hidden** - No variable drop-down list is displayed on the dashboard. 1. Click one of the following links to complete the steps for adding your selected variable type: - [Query](#add-a-query-variable) @@ -171,6 +172,7 @@ Query expressions are different for each data source. For more information, refe - If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand. 1. (Optional) In the **Regex** field, type a regular expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with a regular expression](#filter-variables-with-regex). +1. Under **Apply regex to**, select **Variable value** or **Display text** to choose where the regex pattern is applied. The default is **Variable value**. 1. In the **Sort** drop-down list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query is used. 1. Under **Refresh**, select when the variable should update options: - **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. diff --git a/docs/sources/visualizations/explore/get-started-with-explore.md b/docs/sources/visualizations/explore/get-started-with-explore.md index b59ac68c686..41f1f8c5c01 100644 --- a/docs/sources/visualizations/explore/get-started-with-explore.md +++ b/docs/sources/visualizations/explore/get-started-with-explore.md @@ -71,8 +71,9 @@ 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. - - **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. + - **Saved queries**: + - **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). + - **Replace query** - Reuse a saved query. - **+ Add query** - Add an additional query. - **+ Add from saved queries** - Add an additional query by reusing a saved query. diff --git a/docs/sources/visualizations/explore/logs-integration.md b/docs/sources/visualizations/explore/logs-integration.md index 8aa18f45616..a4f8fe7c64b 100644 --- a/docs/sources/visualizations/explore/logs-integration.md +++ b/docs/sources/visualizations/explore/logs-integration.md @@ -43,24 +43,36 @@ If the data source doesn't support loading the full range logs volume, the logs The following sections provide detailed explanations on how to visualize and interact with individual logs in Explore. -### Logs navigation +### Infinite scroll -Logs navigation, located at the right side of the log lines, can be used to easily request additional logs by clicking **Older logs** at the bottom of the navigation. This is especially useful when you reach the line limit and you want to see more logs. Each request run from the navigation displays in the navigation as separate page. Every page shows `from` and `to` timestamps of the incoming log lines. You can see previous results by clicking on each page. Explore caches the last five requests run from the logs navigation so you're not re-running the same queries when clicking on the pages, saving time and resources. + -![Navigate logs in Explore](/static/img/docs/explore/navigate-logs-8-0.png) +When you reach the bottom of the list of logs, you will see the message `Scroll to load more`. If you continue scrolling and the displayed logs are within the selected time interval, Grafana will 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. + + ### Visualization options You have the option to customize the display of logs and choose which columns to show. Following is a list of available options. -| Option | Description | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Time** | Shows or hides the time column. This is the timestamp associated with the log line as reported from the data source. | -| **Unique labels** | Shows or hides the unique labels column that includes only non-common labels. All common labels are displayed above. | -| **Wrap lines** | Set this to `true` if you want the display to use line wrapping. If set to `false`, it will result in horizontal scrolling. | -| **Prettify JSON** | Set this to `true` to pretty print all JSON logs. This setting does not affect logs in any format other than JSON. | -| **Deduplication** | Log data can be very repetitive. Explore hides duplicate log lines using a few different deduplication algorithms. **Exact** matches are done on the whole line except for date fields. **Numbers** matches are done on the line after stripping out numbers such as durations, IP addresses, and so on. **Signature** is the most aggressive deduplication as it strips all letters and numbers and matches on the remaining whitespace and punctuation. | -| **Display results order** | You can change the order of received logs from the default descending order (newest first) to ascending order (oldest first). | + + +| Option | Description | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Expand / Collapse | Expand or collapse the controls toolbar. | +| Scroll to bottom | Jump to the bottom of the logs table. | +| Oldest Logs First / Newest logs first | Sort direction (ascending or descending). | +| Search logs / Close search | Click to open/close the client side string search of the displayed logs result. | +| Deduplication | **None** does not perform any deduplication, **Exact** matches are done on the whole line except for date fields. **Numbers** matches are done on the line after stripping out numbers such as durations, IP addresses, and so on. **Signature** is the most aggressive deduplication as it strips all letters and numbers and matches on the remaining whitespace and punctuation. | +| Filter levels | Filter logs in display by log level: All levels, Info, Debut, Warning, Error. | +| Set Timestamp format | Hide timestamps (disabled), Show milliseconds timestamps, Show nanoseconds timestamps. | +| Set line wrap | Disable line wrapping, Enable line wrapping, Enable line wrapping and prettify JSON. | +| Enable highlighting | Plain text, Highlight text. | +| Font size | Small font (default), Large font. | +| Unescaped newlines | Only displayed if the logs contain unescaped new lines. Click to unescape and display as new lines. | +| Download logs | Plain text (txt), JavaScript Object Notation (JSON), Comma-separated values (CSV) | + + ### Download log lines @@ -143,16 +155,31 @@ Click the **eye icon** to select a subset of fields to visualize in the logs lis Each field has a **stats icon**, which displays ad-hoc statistics in relation to all displayed logs. +For data sources that support log types, such as Loki, instead of a single view containing all fields, fields will be displayed grouped by their type: Indexed Labels, Parsed fields, and Structured Metadata. + #### Links Grafana provides data links or correlations, allowing you to convert any part of a log message into an internal or external link. These links enable you to navigate to related data or external resources, offering a seamless and convenient way to explore additional information. {{< figure src="/static/img/docs/explore/data-link-9-4.png" max-width="800px" caption="Data link in Explore" >}} +#### Log details modes + +There are two modes available to view log details: + +- **Inline** The default, displays log details below the log line. +- **Sidebar** Displays log details in a sidebar view. + +No matter which display mode you are currently viewing, you can change it by clicking the mode control icon. + ### Log context Log context is a feature that displays additional lines of context surrounding a log entry that matches a specific search query. This helps in understanding the context of the log entry and is similar to the `-C` parameter in the `grep` command. +If you're using Loki for your logs, to modify your log context queries, you can use the Loki log context query editor at the top of the table. You can activate this editor by clicking the menu for the log line, and selecting **Show context**. Within the **Log Context** view, you have the option to modify your search by removing one or more label filters from the log stream. If your original query used a parser, you can refine your search by leveraging extracted label filters. + +Change the **Context time window** option to look for logs within a specific time interval around your log line. + Toggle **Wrap lines** if you encounter long lines of text that make it difficult to read and analyze the context around log entries. By enabling this toggle, Grafana automatically wraps long lines of text to fit within the visible width of the viewer, making the log entries easier to read and understand. Click **Open in split view** to execute the context query for a log entry in a split screen in the Explore view. Clicking this button opens a new Explore pane with the context query displayed alongside the log entry, making it easier to analyze and understand the surrounding context. 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 4afbd8f71a6..f219f10a68a 100644 --- a/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md +++ b/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md @@ -88,8 +88,9 @@ 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. - - **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. + - **Saved queries**: + - **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). + - **Replace query** - Reuse a saved query. - **+ Add query** - Add an additional query. - **+ Add from saved queries** - Add an additional query by reusing a saved query. 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 b6c9471e3e3..0aafe41f524 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md @@ -156,17 +156,23 @@ 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. -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** or open the **Saved queries** drop-down menu and click **Replace query** in the query editor: {{< figure src="/media/docs/grafana/dashboards/screenshot-use-saved-queries-v12.3.png" max-width="750px" alt="Access saved queries" >}} -Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** updates your existing query. +Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace query** in the **Saved queries** drop-down menu updates your existing query. + +{{< admonition type="note" >}} +Users with Admin and Editor roles can create and save queries for reuse. +While Admin users can edit or delete any saved queries, users with the Editor role can only edit or delete the queries they've saved. +Viewers can only reuse queries. +{{< /admonition >}} #### Save a query To save a query you've created: -1. From the query editor, click the **Save query** icon: +1. From the query editor, open the **Saved queries** drop-down menu and click the **Save query** option: {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-save-query-v12.2.png" max-width="750px" alt="Save a query" >}} @@ -221,7 +227,7 @@ To add a query, follow these steps: 1. To create a query, do one of the following: - Write or construct a query in the query language of your data source. - - Click **Replace with saved query** to reuse a saved query. + - Open the **Saved queries** drop-down menu and click **Replace 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. @@ -229,7 +235,7 @@ 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) To [save the query](#save-a-query) for reuse, click the **Save query** option in the **Saved queries** drop-down menu. 1. (Optional) Click **+ Add query** or **Add from saved queries** to add more queries as needed. 1. Click **Run queries**. diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md index b6b3499b4d7..2a13836d246 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md @@ -12,12 +12,13 @@ comments: | To build this Markdown, do the following: $ cd /docs (from the root of the repository) - $ make sources/panels-visualizations/query-transform-data/transform-data/index.md + $ make sources/visualizations/panels-visualizations/query-transform-data/transform-data/index.md $ make docs Browse to http://localhost:3003/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/ Refer to ./docs/README.md "Content guidelines" for more information about editing and building these docs. + aliases: - ../../../panels/transform-data/ # /docs/grafana/next/panels/transform-data/ - ../../../panels/transform-data/about-transformation/ # /docs/grafana/next/panels/transform-data/about-transformation/ diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md index 3af0eeda0d5..d3a93095e7a 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md @@ -31,7 +31,7 @@ refs: _Logs_ are structured records of events or messages generated by a system or application—that is, a series of text records with status updates from your system or app. They generally include timestamps, messages, and context information like the severity of the logged event. -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. +The logs visualization displays these records from data sources that support logs, such as Elastic, Influx, and Loki. The logs visualization shows, by default, the timestamp, a colored string representing the log status, the log line body, as well as collapsible log events that help you analyze the information generated. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-logs-v12.3.png" max-width="750px" alt="Logs visualization" >}} @@ -100,16 +100,16 @@ Use these settings to refine your visualization: | Option | Description | | --------------- | --------------- | -| Time | Show or hide the time column. This is the timestamp associated with the log line as reported from the data source. | +| Show timestamps | 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. | -| 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. | +| 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 highlighting | Use a predefined syntax coloring grammar to highlight relevant parts of the log lines | | 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.| +| Log Details panel mode | Choose to display the log details in a sidebar panel or inline, below the log line. | +| Enable infinite scrolling | Request more results by scrolling to the bottom of the logs list. | +| Show controls | Display controls to jump to the last or first log line, and filters by log level | +| Font size | Select between the default font size and small font size. | | 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/e2e-playwright/alerting-suite/saved-searches.spec.ts b/e2e-playwright/alerting-suite/saved-searches.spec.ts new file mode 100644 index 00000000000..28de805a5a1 --- /dev/null +++ b/e2e-playwright/alerting-suite/saved-searches.spec.ts @@ -0,0 +1,271 @@ +import { Page } from '@playwright/test'; + +import { test, expect } from '@grafana/plugin-e2e'; + +/** + * UI selectors for Saved Searches e2e tests. + * Each selector is a function that takes the page and returns a locator. + */ +const ui = { + // Main elements + savedSearchesButton: (page: Page) => page.getByRole('button', { name: /saved searches/i }), + dropdown: (page: Page) => page.getByRole('dialog', { name: /saved searches/i }), + searchInput: (page: Page) => page.getByTestId('search-query-input'), + + // Save functionality + saveButton: (page: Page) => page.getByRole('button', { name: /save current search/i }), + saveConfirmButton: (page: Page) => page.getByRole('button', { name: /^save$/i }), + saveNameInput: (page: Page) => page.getByPlaceholder(/enter a name/i), + + // Action menu + actionsButton: (page: Page) => page.getByRole('button', { name: /actions/i }), + renameMenuItem: (page: Page) => page.getByText(/rename/i), + deleteMenuItem: (page: Page) => page.getByText(/^delete$/i), + setAsDefaultMenuItem: (page: Page) => page.getByText(/set as default/i), + deleteConfirmButton: (page: Page) => page.getByRole('button', { name: /^delete$/i }), + + // Indicators + emptyState: (page: Page) => page.getByText(/no saved searches/i), + defaultIcon: (page: Page) => page.locator('[title="Default search"]'), + duplicateError: (page: Page) => page.getByText(/already exists/i), +}; + +/** + * Helper to clear saved searches storage. + * UserStorage uses localStorage as fallback, so we clear both potential keys. + */ +async function clearSavedSearches(page: Page) { + await page.evaluate(() => { + // Clear localStorage keys that might contain saved searches + // UserStorage stores under 'grafana.userstorage.alerting' pattern + const keysToRemove = Object.keys(localStorage).filter( + (key) => key.includes('alerting') && (key.includes('savedSearches') || key.includes('userstorage')) + ); + keysToRemove.forEach((key) => localStorage.removeItem(key)); + + // Also clear session storage visited flag + const sessionKeysToRemove = Object.keys(sessionStorage).filter((key) => key.includes('alerting')); + sessionKeysToRemove.forEach((key) => sessionStorage.removeItem(key)); + }); +} + +test.describe( + 'Alert Rules - Saved Searches', + { + tag: ['@alerting'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Clear any saved searches from previous tests before navigating + await page.goto('/alerting/list'); + await clearSavedSearches(page); + await page.reload(); + }); + + test.afterEach(async ({ page }) => { + // Clean up saved searches after each test + await clearSavedSearches(page); + }); + + test('should display Saved searches button', async ({ page }) => { + await expect(ui.savedSearchesButton(page)).toBeVisible(); + }); + + test('should open dropdown when clicking Saved searches button', async ({ page }) => { + await ui.savedSearchesButton(page).click(); + + await expect(ui.dropdown(page)).toBeVisible(); + }); + + test('should show empty state when no saved searches exist', async ({ page }) => { + // Storage is cleared in beforeEach, so we should see empty state + await ui.savedSearchesButton(page).click(); + + await expect(ui.emptyState(page)).toBeVisible(); + }); + + test('should enable Save current search button when search query is entered', async ({ page }) => { + // Enter a search query + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + // Open saved searches + await ui.savedSearchesButton(page).click(); + + await expect(ui.saveButton(page)).toBeEnabled(); + }); + + test('should disable Save current search button when search query is empty', async ({ page }) => { + await ui.savedSearchesButton(page).click(); + + await expect(ui.saveButton(page)).toBeDisabled(); + }); + + test('should save a new search', async ({ page }) => { + // Enter a search query + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + // Open saved searches + await ui.savedSearchesButton(page).click(); + + // Click save button + await ui.saveButton(page).click(); + + // Enter name and save + await ui.saveNameInput(page).fill('My Firing Rules'); + await ui.saveConfirmButton(page).click(); + + // Verify the saved search appears in the list + await expect(page.getByText('My Firing Rules')).toBeVisible(); + }); + + test('should show validation error for duplicate name', async ({ page }) => { + // First save a search + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + await ui.saveButton(page).click(); + + await ui.saveNameInput(page).fill('Duplicate Test'); + await ui.saveConfirmButton(page).click(); + + // Try to save another with the same name + await ui.saveButton(page).click(); + await ui.saveNameInput(page).fill('Duplicate Test'); + await ui.saveConfirmButton(page).click(); + + // Verify validation error + await expect(ui.duplicateError(page)).toBeVisible(); + }); + + test('should apply a saved search', async ({ page }) => { + // Create a saved search first + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + await ui.saveButton(page).click(); + + await ui.saveNameInput(page).fill('Apply Test'); + await ui.saveConfirmButton(page).click(); + + // Clear the search + await ui.searchInput(page).clear(); + await ui.searchInput(page).press('Enter'); + + // Apply the saved search + await ui.savedSearchesButton(page).click(); + await page.getByRole('button', { name: /apply search.*apply test/i }).click(); + + // Verify the search input is updated + await expect(ui.searchInput(page)).toHaveValue('state:firing'); + }); + + test('should rename a saved search', async ({ page }) => { + // Create a saved search + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + await ui.saveButton(page).click(); + + await ui.saveNameInput(page).fill('Original Name'); + await ui.saveConfirmButton(page).click(); + + // Open action menu and click rename + await ui.actionsButton(page).click(); + await ui.renameMenuItem(page).click(); + + // Enter new name + const renameInput = page.getByDisplayValue('Original Name'); + await renameInput.clear(); + await renameInput.fill('Renamed Search'); + await page.keyboard.press('Enter'); + + // Verify the name was updated + await expect(page.getByText('Renamed Search')).toBeVisible(); + await expect(page.getByText('Original Name')).not.toBeVisible(); + }); + + test('should delete a saved search', async ({ page }) => { + // Create a saved search + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + await ui.saveButton(page).click(); + + await ui.saveNameInput(page).fill('To Delete'); + await ui.saveConfirmButton(page).click(); + + // Verify it was saved + await expect(page.getByText('To Delete')).toBeVisible(); + + // Open action menu and click delete + await ui.actionsButton(page).click(); + await ui.deleteMenuItem(page).click(); + + // Confirm delete + await ui.deleteConfirmButton(page).click(); + + // Verify it was deleted + await expect(page.getByText('To Delete')).not.toBeVisible(); + }); + + test('should set a search as default', async ({ page }) => { + // Create a saved search + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + await ui.saveButton(page).click(); + + await ui.saveNameInput(page).fill('Default Test'); + await ui.saveConfirmButton(page).click(); + + // Set as default + await ui.actionsButton(page).click(); + await ui.setAsDefaultMenuItem(page).click(); + + // Verify the star icon appears (indicating default) + await expect(ui.defaultIcon(page)).toBeVisible(); + }); + + test('should close dropdown when pressing Escape', async ({ page }) => { + await ui.savedSearchesButton(page).click(); + + await expect(ui.dropdown(page)).toBeVisible(); + + await page.keyboard.press('Escape'); + + await expect(ui.dropdown(page)).not.toBeVisible(); + }); + + test('should cancel save mode when pressing Escape', async ({ page }) => { + // Enter a search query + await ui.searchInput(page).fill('state:firing'); + await ui.searchInput(page).press('Enter'); + + await ui.savedSearchesButton(page).click(); + + // Start save mode + await ui.saveButton(page).click(); + + await expect(ui.saveNameInput(page)).toBeVisible(); + + // Press Escape to cancel + await page.keyboard.press('Escape'); + + // Verify we're back to list mode + await expect(ui.saveNameInput(page)).not.toBeVisible(); + await expect(ui.saveButton(page)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboard-cujs/scope-redirect.spec.ts b/e2e-playwright/dashboard-cujs/scope-redirect.spec.ts index c0e6e55b5b0..952e8a3da63 100644 --- a/e2e-playwright/dashboard-cujs/scope-redirect.spec.ts +++ b/e2e-playwright/dashboard-cujs/scope-redirect.spec.ts @@ -8,6 +8,7 @@ test.use({ scopeFilters: true, groupByVariable: true, reloadDashboardsOnParamsChange: true, + useScopesNavigationEndpoint: true, }, }); @@ -61,31 +62,6 @@ test.describe('Scope Redirect Functionality', () => { }); }); - test('should fall back to scope navigation when no redirectUrl', async ({ page, gotoDashboardPage }) => { - const scopes = testScopesWithRedirect(); - - await test.step('Navigate to dashboard and open scopes selector', async () => { - await gotoDashboardPage({ uid: 'cuj-dashboard-1' }); - await openScopesSelector(page, scopes); - }); - - await test.step('Select scope without redirectUrl', async () => { - // Select the scope without redirectUrl directly - await selectScope(page, 'sn-redirect-fallback', scopes[1]); - }); - - await test.step('Apply scopes and verify fallback behavior', async () => { - await applyScopes(page, [scopes[1]]); - - // Should stay on current dashboard since no redirectUrl is provided - // The scope navigation fallback should not redirect (as per existing behavior) - await expect(page).toHaveURL(/\/d\/cuj-dashboard-1/); - - // Verify the scope was applied - await expect(page).toHaveURL(/scopes=scope-sn-redirect-fallback/); - }); - }); - test('should not redirect when reloading page on dashboard not in dashboard list', async ({ page, gotoDashboardPage, @@ -171,4 +147,47 @@ test.describe('Scope Redirect Functionality', () => { await expect(page).not.toHaveURL(/scopes=/); }); }); + + test('should not redirect to redirectPath when on active scope navigation', async ({ page, gotoDashboardPage }) => { + const scopes = testScopesWithRedirect(); + + await test.step('Set up scope navigation to dashboard-1', async () => { + // First, apply a scope that creates scope navigation to dashboard-1 (without redirectPath) + await gotoDashboardPage({ uid: 'cuj-dashboard-1' }); + await openScopesSelector(page, scopes); + await selectScope(page, 'sn-redirect-setup', scopes[2]); + await applyScopes(page, [scopes[2]]); + + // Verify we're on dashboard-1 with the scope applied + await expect(page).toHaveURL(/\/d\/cuj-dashboard-1/); + await expect(page).toHaveURL(/scopes=scope-sn-redirect-setup/); + }); + + await test.step('Navigate to dashboard-1 to be on active scope navigation', async () => { + // Navigate to dashboard-1 which is now a scope navigation target + await gotoDashboardPage({ + uid: 'cuj-dashboard-1', + queryParams: new URLSearchParams({ scopes: 'scope-sn-redirect-setup' }), + }); + + // Verify we're on dashboard-1 + await expect(page).toHaveURL(/\/d\/cuj-dashboard-1/); + }); + + await test.step('Apply scope with redirectPath and verify no redirect', async () => { + // Now apply a different scope that has redirectPath + // Since we're on an active scope navigation, it should NOT redirect + await openScopesSelector(page, scopes); + await selectScope(page, 'sn-redirect-with-navigation', scopes[3]); + await applyScopes(page, [scopes[3]]); + + // Verify the new scope was applied + await expect(page).toHaveURL(/scopes=scope-sn-redirect-with-navigation/); + + // Since we're already on the active scope navigation (dashboard-1), + // we should NOT redirect to redirectPath (dashboard-3) + await expect(page).toHaveURL(/\/d\/cuj-dashboard-1/); + await expect(page).not.toHaveURL(/\/d\/cuj-dashboard-3/); + }); + }); }); 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 c35eb03bf84..ad954267c85 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -419,6 +419,9 @@ test.describe( // Select tabs layout await page.getByLabel('layout-selection-option-Tabs').click(); + // confirm layout change + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); await expect( @@ -757,6 +760,9 @@ test.describe( // Select rows layout await page.getByLabel('layout-selection-option-Rows').click(); + // confirm layout change + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + await dashboardPage .getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1')) .scrollIntoViewIfNeeded(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-keybindings.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-keybindings.spec.ts new file mode 100644 index 00000000000..19ad38f16d5 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-keybindings.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + }, +}); + +test.describe('Dashboard keybindings with new layouts', { tag: ['@dashboards'] }, () => { + test.use({ + viewport: { width: 1280, height: 1080 }, + }); + + test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' }); + + const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContents).toHaveCount(5); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob')) + ).toBeVisible(); + + // Collapse all rows using keyboard shortcut: d + Shift+C + await page.keyboard.press('d'); + await page.keyboard.press('Shift+C'); + + await expect(panelContents).toHaveCount(0); + await expect(page.getByText('server = A, pod = Bob')).toBeHidden(); + await expect(page.getByText('server = B, pod = Bob')).toBeHidden(); + + // Expand all rows using keyboard shortcut: d + Shift+E + await page.keyboard.press('d'); + await page.keyboard.press('Shift+E'); + + await expect(panelContents).toHaveCount(6); + await expect(page.getByText('server = A, pod = Bob')).toBeVisible(); + await expect(page.getByText('server = B, pod = Bob')).toBeVisible(); + }); + + test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' }); + + // Find Panel #1 and press 'i' to open inspector + const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1')); + await expect(panel1).toBeVisible(); + await panel1.press('i'); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + + // Press Escape to close inspector + await page.keyboard.press('Escape'); + + await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden(); + }); +}); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index ffacaa13912..803f9d18d46 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -4,6 +4,8 @@ import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin- import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; +import { switchToAutoGrid } from './utils'; + test.use({ featureToggles: { kubernetesDashboards: true, @@ -33,7 +35,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await expect( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) @@ -64,7 +67,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); // Get initial positions - standard width should have panels on different rows const firstPanelTop = await getPanelTop(dashboardPage, selectors); @@ -124,7 +128,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) @@ -181,7 +186,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -216,7 +222,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -271,7 +278,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -328,7 +336,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); // Set narrow column width first to ensure panels fit horizontally await dashboardPage diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts index dbba5c583c1..3e25adc6c41 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts @@ -1,6 +1,6 @@ import { Page } from 'playwright-core'; -import { test, expect } from '@grafana/plugin-e2e'; +import { test, expect, DashboardPage } from '@grafana/plugin-e2e'; import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json'; @@ -12,6 +12,7 @@ import { getPanelPosition, importTestDashboard, goToEmbeddedPanel, + switchToAutoGrid, } from './utils'; const repeatTitleBase = 'repeat - '; @@ -42,7 +43,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().click(); @@ -78,7 +79,8 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); + await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -117,7 +119,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); // select first/original repeat panel to activate edit pane await dashboardPage @@ -148,7 +150,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -214,7 +216,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); // loading directly into panel editor @@ -271,7 +273,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); // this moving repeated panel between two normal panels await movePanel(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`, 'New panel'); @@ -319,7 +321,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -382,7 +384,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -410,7 +412,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -462,7 +464,3 @@ test.describe( }); } ); - -async function switchToAutoGrid(page: Page) { - await page.getByLabel('layout-selection-option-Auto grid').click(); -} diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 83508063ef5..69851994812 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -1,5 +1,6 @@ import { Page } from '@playwright/test'; +import { selectors } from '@grafana/e2e-selectors'; import { DashboardPage, E2ESelectorGroups, expect } from '@grafana/plugin-e2e'; import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; @@ -239,3 +240,12 @@ export async function getTabPosition(dashboardPage: DashboardPage, selectors: E2 const boundingBox = await tab.boundingBox(); return boundingBox; } + +export async function switchToAutoGrid(page: Page, dashboardPage: DashboardPage) { + await page.getByLabel('layout-selection-option-Auto grid').click(); + // confirm layout change if applicable + const confirmModal = dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete); + if (confirmModal) { + await confirmModal.click(); + } +} diff --git a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts index 6765d299e53..caa83bfdb90 100644 --- a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @@ -10,7 +10,7 @@ const NUM_NESTED_DASHBOARDS = 60; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts index 8ae318bcefa..6949eca4555 100644 --- a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @@ -5,7 +5,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts index a97a04a14b8..e15991514a2 100644 --- a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts @@ -7,7 +7,7 @@ test.use({ scenes: true, sharingDashboardImage: true, // Enable the export image feature kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts index 428193ab5fa..26a8fb61dc8 100644 --- a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts index b0ecf44f9f1..f874cefa27c 100644 --- a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts index 0a982e148b5..ca05fd24160 100644 --- a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -5,7 +5,7 @@ import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts index 20f455ea3a8..b7e18e56b45 100644 --- a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts @@ -5,7 +5,7 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts index fd5dc979d81..9653218f5ff 100644 --- a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts index c59e323076d..9f15a740fcf 100644 --- a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts index 3398e9aaa35..3827872c199 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -4,7 +4,7 @@ test.use({ featureToggles: { scenes: true, kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts index 26f8b85d13e..6ff1825f9cd 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts @@ -4,7 +4,7 @@ test.use({ featureToggles: { scenes: true, kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts index 1a7e03d6243..b5c77458121 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -6,7 +6,7 @@ test.use({ featureToggles: { scenes: true, kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts index 78c35dc5de7..0a4eb5d3a9a 100644 --- a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -6,7 +6,7 @@ test.use({ timezoneId: 'Pacific/Easter', featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts index 937224290b0..ee3de574512 100644 --- a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts @@ -8,7 +8,7 @@ const TIMEZONE_DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts index 4ffd65b83a3..ae2f08b230f 100644 --- a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -17,7 +17,7 @@ test.use({ }, featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts index f457eddf19e..aad8ed3367a 100644 --- a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts +++ b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts index 99f84cb6d9f..d3ba9d9046e 100644 --- a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts +++ b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts index 5fdca6954aa..f489f576887 100644 --- a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts +++ b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts @@ -5,7 +5,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts index ca06e31528a..08847be8d8e 100644 --- a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts index fa0b5fc1bfd..0abd9d248f1 100644 --- a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts @@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts index c14a952e1d9..79e545c9a41 100644 --- a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts @@ -53,7 +53,7 @@ async function assertPreviewValues( test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts index cc19e67cda4..03859ccc5ea 100644 --- a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts @@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts index d76b5291c42..9c5cc8cc60f 100644 --- a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts @@ -19,7 +19,7 @@ async function assertPreviewValues( test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts index ed92c79ee36..96375f3fb97 100644 --- a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Templating - Nested Template Variables'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); @@ -79,6 +79,16 @@ test.describe( await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); await expect(regexInput).toHaveValue(''); + // Check regex apply to field - should default to "Variable value" + const regexApplyToField = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2 + ); + await expect(regexApplyToField).toBeVisible(); + const variableValueRadio = page.getByRole('radio', { name: 'Variable value' }); + await expect(variableValueRadio).toBeChecked(); + const displayTextRadio = page.getByRole('radio', { name: 'Display text' }); + await expect(displayTextRadio).not.toBeChecked(); + const sortSelect = dashboardPage.getByGrafanaSelector( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 ); diff --git a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts index c669dc563c4..ba3b9466e7d 100644 --- a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts @@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts index a55f14b8643..413466972e1 100644 --- a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts index bb188c87e8d..53966eadf05 100644 --- a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts index e31c5792062..06c0e77989b 100644 --- a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts index 53290345e73..629abda2ce3 100644 --- a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts @@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts index 123aa7f3279..50febc211b8 100644 --- a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts +++ b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts @@ -5,7 +5,7 @@ const DASHBOARD_UID = 'ZqZnVvFZz'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 1d8fd32ff06..1806aca24bf 100644 --- a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -5,7 +5,7 @@ const DASHBOARD_UID = 'yBCC3aKGk'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts index 4fb56ef8b8e..b78e781dad1 100644 --- a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts +++ b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts @@ -7,7 +7,7 @@ const PAGE_UNDER_TEST = 'AejrN1AMz'; test.use({ featureToggles: { kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', - kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', + dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/panels-suite/canvas-scene.spec.ts b/e2e-playwright/panels-suite/canvas-scene.spec.ts index b1fc028f3ae..c0b392d544b 100644 --- a/e2e-playwright/panels-suite/canvas-scene.spec.ts +++ b/e2e-playwright/panels-suite/canvas-scene.spec.ts @@ -2,18 +2,16 @@ import { Locator } from '@playwright/test'; import { test, expect } from '@grafana/plugin-e2e'; -import { setVisualization } from './vizpicker-utils'; - test.use({ featureToggles: { canvasPanelPanZoom: true, }, }); test.describe('Canvas Panel - Scene Tests', () => { - test.beforeEach(async ({ page, gotoDashboardPage, selectors }) => { + test.beforeEach(async ({ page, gotoDashboardPage }) => { const dashboardPage = await gotoDashboardPage({}); const panelEditPage = await dashboardPage.addPanel(); - await setVisualization(panelEditPage, 'Canvas', selectors); + await panelEditPage.setVisualization('Canvas'); // Wait for canvas panel to load await page.waitForSelector('[data-testid="canvas-scene-pan-zoom"]', { timeout: 10000 }); diff --git a/e2e-playwright/panels-suite/gauge.spec.ts b/e2e-playwright/panels-suite/gauge.spec.ts new file mode 100644 index 00000000000..cb4b26e9850 --- /dev/null +++ b/e2e-playwright/panels-suite/gauge.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport so all gauge panels load properly +test.use({ + featureToggles: { newGauge: true }, + viewport: { width: 1280, height: 3000 }, +}); + +const OLD_GAUGES_DASHBOARD_UID = '_5rDmaQiz'; +const NEW_GAUGES_DASHBOARD_UID = 'panel-tests-gauge-new'; + +test.describe( + 'Gauge Panel', + { + tag: ['@panels', '@gauge'], + }, + () => { + test('successfully migrates all gauge panels', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: OLD_GAUGES_DASHBOARD_UID }); + + // check that gauges are rendered + const gaugeElements = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Visualization.Gauge.Container + ); + await expect(gaugeElements).toHaveCount(16); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo).toBeHidden(); + }); + + test('renders new gauge panels', async ({ gotoDashboardPage, selectors }) => { + // open Panel Tests - Gauge + const dashboardPage = await gotoDashboardPage({ uid: NEW_GAUGES_DASHBOARD_UID }); + + // check that gauges are rendered + const gaugeElements = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Visualization.Gauge.Container + ); + await expect(gaugeElements).toHaveCount(32); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo).toBeHidden(); + }); + + test('renders sparklines in gauge panels', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: NEW_GAUGES_DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '11' }), + }); + + await expect(page.locator('.uplot')).toHaveCount(5); + }); + + test('"no data"', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: NEW_GAUGES_DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '36' }), + }); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container), + 'that the gauge does not appear' + ).toBeHidden(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage), + '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( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container), + 'that the empty text shows up in an empty gauge' + ).toHaveText('My empty value'); + + // test the "no numeric fields" message on the next panel + const dashboardPage2 = await gotoDashboardPage({ + uid: NEW_GAUGES_DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '37' }), + }); + + await expect( + dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container), + 'that the gauge does not appear' + ).toBeHidden(); + + await expect( + dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage), + 'that the empty text appears' + ).toHaveText('Data is missing a number field'); + }); + } +); diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index a14085aa753..6dddba81820 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -343,6 +343,33 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] // TODO -- saving for another day. }); + test('Tests nested table expansion', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '4' }), + }); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Nested tables')) + ).toBeVisible(); + + await waitForTableLoad(page); + + await expect(page.locator('[role="row"]')).toHaveCount(3); // header + 2 rows + + const firstRowExpander = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.RowExpander) + .first(); + + await firstRowExpander.click(); + await expect(page.locator('[role="row"]')).not.toHaveCount(3); // more rows are present now, it is dynamic tho. + + // TODO: test sorting + + await firstRowExpander.click(); + await expect(page.locator('[role="row"]')).toHaveCount(3); // back to original state + }); + test('Tests tooltip interactions', async ({ gotoDashboardPage, selectors }) => { const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID, diff --git a/e2e-playwright/panels-suite/vizpicker-utils.ts b/e2e-playwright/panels-suite/vizpicker-utils.ts deleted file mode 100644 index 1785dd7e04a..00000000000 --- a/e2e-playwright/panels-suite/vizpicker-utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, E2ESelectorGroups, PanelEditPage } from '@grafana/plugin-e2e'; - -// this replaces the panelEditPage.setVisualization method used previously in tests, since it -// does not know how to use the updated 12.4 viz picker UI to set the visualization -export const setVisualization = async (panelEditPage: PanelEditPage, vizName: string, selectors: E2ESelectorGroups) => { - const vizPicker = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); - await expect(vizPicker, '"Change" button should be visible').toBeVisible(); - await vizPicker.click(); - - const allVizTabBtn = panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations')); - await expect(allVizTabBtn, '"All visualiations" button should be visible').toBeVisible(); - await allVizTabBtn.click(); - - const vizItem = panelEditPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(vizName)); - await expect(vizItem, `"${vizName}" item should be visible`).toBeVisible(); - await vizItem.scrollIntoViewIfNeeded(); - await vizItem.click(); - - await expect(vizPicker, '"Change" button should be visible again').toBeVisible(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), - 'Panel header should have the new viz type name' - ).toHaveText(vizName); -}; 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 336dbef0a29..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 @@ -1,6 +1,5 @@ import { expect, test } from '@grafana/plugin-e2e'; -import { setVisualization } from '../../../panels-suite/vizpicker-utils'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; @@ -25,10 +24,10 @@ test.describe( ).toContainText(['Field', 'Max', 'Mean', 'Last']); }); - test('table panel data assertions', async ({ panelEditPage, selectors }) => { + test('table panel data assertions', async ({ panelEditPage }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await setVisualization(panelEditPage, 'Table', selectors); + await panelEditPage.setVisualization('Table'); await panelEditPage.refreshPanel(); await expect( panelEditPage.panel.locator, @@ -44,10 +43,10 @@ test.describe( ).toContainText(['val1', 'val2', 'val3', 'val4']); }); - test('timeseries panel - table view assertions', async ({ panelEditPage, selectors }) => { + test('timeseries panel - table view assertions', async ({ panelEditPage }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await setVisualization(panelEditPage, 'Time series', selectors); + await panelEditPage.setVisualization('Time series'); await panelEditPage.refreshPanel(); await panelEditPage.toggleTableView(); await expect( diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts index 93e0525ab0e..46c36277848 100644 --- a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -1,6 +1,5 @@ import { expect, test } from '@grafana/plugin-e2e'; -import { setVisualization } from '../../../panels-suite/vizpicker-utils'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; import { scenarios } from '../mocks/resources'; @@ -54,10 +53,10 @@ test.describe( ).toHaveText(scenarios.map((s) => s.name)); }); - test('mocked query data response', async ({ panelEditPage, page, selectors }) => { + test('mocked query data response', async ({ panelEditPage, page }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); await panelEditPage.refreshPanel(); await expect( panelEditPage.panel.getErrorIcon(), @@ -76,7 +75,7 @@ test.describe( selectors, page, }) => { - await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); await expect( panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), formatExpectError('Expected panel visualization to be set to table') @@ -93,8 +92,8 @@ test.describe( ).toBeVisible(); }); - test('Select time zone in timezone picker', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('Select time zone in timezone picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = await panelEditPage.getCustomOptions('Axis'); const timeZonePicker = axisOptions.getSelect('Time zone'); @@ -102,8 +101,8 @@ test.describe( await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); }); - test('select unit in unit picker', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('select unit in unit picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const standardOptions = panelEditPage.getStandardOptions(); const unitPicker = standardOptions.getUnitPicker('Unit'); @@ -112,8 +111,8 @@ test.describe( await expect(unitPicker).toHaveSelected('Pixels'); }); - test('enter value in number input', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('enter value in number input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = panelEditPage.getCustomOptions('Axis'); const lineWith = axisOptions.getNumberInput('Soft min'); @@ -122,8 +121,8 @@ test.describe( await expect(lineWith).toHaveValue('10'); }); - test('enter value in slider', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('enter value in slider', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const graphOptions = panelEditPage.getCustomOptions('Graph styles'); const lineWidth = graphOptions.getSliderInput('Line width'); @@ -132,8 +131,8 @@ test.describe( await expect(lineWidth).toHaveValue('10'); }); - test('select value in single value select', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('select value in single value select', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const standardOptions = panelEditPage.getStandardOptions(); const colorSchemeSelect = standardOptions.getSelect('Color scheme'); @@ -141,8 +140,8 @@ test.describe( await expect(colorSchemeSelect).toHaveSelected('Classic palette'); }); - test('clear input', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('clear input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const panelOptions = panelEditPage.getPanelOptions(); const title = panelOptions.getTextInput('Title'); @@ -151,8 +150,8 @@ test.describe( await expect(title).toHaveValue(''); }); - test('enter value in input', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('enter value in input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const panelOptions = panelEditPage.getPanelOptions(); const description = panelOptions.getTextInput('Description'); @@ -161,8 +160,8 @@ test.describe( await expect(description).toHaveValue('This is a panel'); }); - test('unchecking switch', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('unchecking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = panelEditPage.getCustomOptions('Axis'); const showBorder = axisOptions.getSwitch('Show border'); @@ -174,8 +173,8 @@ test.describe( await expect(showBorder).toBeChecked({ checked: false }); }); - test('checking switch', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('checking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = panelEditPage.getCustomOptions('Axis'); const showBorder = axisOptions.getSwitch('Show border'); @@ -184,8 +183,8 @@ test.describe( await expect(showBorder).toBeChecked(); }); - test('re-selecting value in radio button group', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('re-selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = panelEditPage.getCustomOptions('Axis'); const placement = axisOptions.getRadioGroup('Placement'); @@ -196,8 +195,8 @@ test.describe( await expect(placement).toHaveChecked('Auto'); }); - test('selecting value in radio button group', async ({ panelEditPage, selectors }) => { - await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); + test('selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); const axisOptions = panelEditPage.getCustomOptions('Axis'); const placement = axisOptions.getRadioGroup('Placement'); diff --git a/e2e-playwright/smoke-tests-suite/panels.spec.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts index 22f6088d99d..0ad50a1aed7 100644 --- a/e2e-playwright/smoke-tests-suite/panels.spec.ts +++ b/e2e-playwright/smoke-tests-suite/panels.spec.ts @@ -1,4 +1,4 @@ -import { BootData } from '@grafana/data'; +import { BootData, PanelPluginMeta } from '@grafana/data'; import { test, expect } from '@grafana/plugin-e2e'; test.describe( @@ -22,7 +22,7 @@ test.describe( await dashboardPage.addPanel(); // Get panel types from window object - const panelTypes = await page.evaluate(() => { + const panelTypes: PanelPluginMeta[] = await page.evaluate(() => { // @grafana/plugin-e2e doesn't export the full bootdata config // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const win = window as typeof window & { grafanaBootData: BootData }; diff --git a/e2e-playwright/utils/scope-helpers.ts b/e2e-playwright/utils/scope-helpers.ts index 749577ecb05..fc88a79d8fa 100644 --- a/e2e-playwright/utils/scope-helpers.ts +++ b/e2e-playwright/utils/scope-helpers.ts @@ -156,13 +156,18 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) { return; } - const url: string = + const dashboardBindingsUrl: string = '**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_dashboard_bindings?' + scopes.map((scope) => `scope=scope-${scope.name}`).join('&'); + const scopeNavigationsUrl: string = + '**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_navigations?' + + scopes.map((scope) => `scope=scope-${scope.name}`).join('&'); + const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', '']; - await page.route(url, async (route) => { + // Mock scope_dashboard_bindings endpoint + await page.route(dashboardBindingsUrl, async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', @@ -215,7 +220,52 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) { }); }); - const responsePromise = page.waitForResponse((response) => response.url().includes(`/find/scope_dashboard_bindings`)); + // Mock scope_navigations endpoint + await page.route(scopeNavigationsUrl, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + apiVersion: 'scope.grafana.app/v0alpha1', + items: scopes.flatMap((scope) => { + const navigations: Array<{ + kind: string; + apiVersion: string; + metadata: { name: string; resourceVersion: string; creationTimestamp: string }; + spec: { url: string; scope: string }; + status: { title: string }; + }> = []; + + // Create a scope navigation if dashboardUid is provided + if (scope.dashboardUid && scope.addLinks) { + navigations.push({ + kind: 'ScopeNavigation', + apiVersion: 'scope.grafana.app/v0alpha1', + metadata: { + name: `scope-${scope.name}-nav`, + resourceVersion: '1', + creationTimestamp: 'stamp', + }, + spec: { + url: `/d/${scope.dashboardUid}`, + scope: `scope-${scope.name}`, + }, + status: { + title: scope.dashboardTitle ?? scope.title, + }, + }); + } + + return navigations; + }), + }), + }); + }); + + const responsePromise = page.waitForResponse( + (response) => + response.url().includes(`/find/scope_dashboard_bindings`) || response.url().includes(`/find/scope_navigations`) + ); const scopeRequestPromises: Array> = []; for (const scope of scopes) { diff --git a/e2e-playwright/utils/scopes.ts b/e2e-playwright/utils/scopes.ts index c5b141d4eb7..73a29a5da95 100644 --- a/e2e-playwright/utils/scopes.ts +++ b/e2e-playwright/utils/scopes.ts @@ -124,5 +124,23 @@ export const testScopesWithRedirect = (): TestScope[] => { dashboardTitle: 'CUJ Dashboard 2', addLinks: true, }, + { + name: 'sn-redirect-setup', + title: 'Setup Navigation', + // No redirectPath - used to set up scope navigation to dashboard-1 + filters: [{ key: 'namespace', operator: 'equals', value: 'setup-nav' }], + dashboardUid: 'cuj-dashboard-1', // Creates scope navigation to this dashboard + dashboardTitle: 'CUJ Dashboard 1', + addLinks: true, + }, + { + name: 'sn-redirect-with-navigation', + title: 'Redirect With Navigation', + redirectPath: '/d/cuj-dashboard-3', // Redirect target + filters: [{ key: 'namespace', operator: 'equals', value: 'redirect-with-nav' }], + dashboardUid: 'cuj-dashboard-1', // Creates scope navigation to this dashboard + dashboardTitle: 'CUJ Dashboard 1', + addLinks: true, + }, ]; }; diff --git a/e2e-playwright/various-suite/gauge.spec.ts b/e2e-playwright/various-suite/gauge.spec.ts deleted file mode 100644 index 3e2d6023c95..00000000000 --- a/e2e-playwright/various-suite/gauge.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -// this test requires a larger viewport so all gauge panels load properly -test.use({ - viewport: { width: 1280, height: 1080 }, -}); - -test.describe( - 'Gauge Panel', - { - tag: ['@various'], - }, - () => { - test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => { - // open Panel Tests - Gauge - const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' }); - - // check that gauges are rendered - const gaugeElements = page.locator('.flot-base'); - await expect(gaugeElements).toHaveCount(16); - - // check that no panel errors exist - const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); - await expect(errorInfo).toBeHidden(); - }); - } -); diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts index 16a722854aa..82b293da581 100644 --- a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -95,7 +95,7 @@ test.describe( await createNewPanelButton.click(); // Close the data source picker modal - const closeButton = page.getByRole('button', { name: 'Close menu' }); + const closeButton = page.getByRole('button', { name: 'Close', exact: true }); await closeButton.click({ force: true }); // Select prom data source from the data source list diff --git a/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts b/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts new file mode 100644 index 00000000000..282ec1314cc --- /dev/null +++ b/e2e-playwright/various-suite/visualization-suggestions-v2.spec.ts @@ -0,0 +1,178 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newVizSuggestions: true, + externalVizSuggestions: false, + }, + viewport: { + width: 800, + height: 1500, + }, +}); + +test.describe( + 'Visualization suggestions v2', + { + tag: ['@various', '@suggestions'], + }, + () => { + test('Should be shown and clickable', async ({ selectors, gotoPanelEditPage }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel' + ).toBeVisible(); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // TODO: in this part of the test, we will change the query and the transforms and observe suggestions being updated. + + // Select a visualization and verify table header is visible from preview + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button disabled since panel has not yet changed' + ).toBeDisabled(); + + // apply the suggestion and verify panel options are visible + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.confirm('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), + 'options pane to be rendered' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button enabled now that panel is dirty' + ).toBeEnabled(); + }); + + test('should not apply suggestion if you navigate toggle the viz picker back off', async ({ + selectors, + gotoPanelEditPage, + }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel;' + ).toBeVisible(); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect( + panelEditPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .getByRole('grid') + .getByRole('row') + .first(), + 'table to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton) + ).toBeDisabled(); + + // Verify that toggling the viz picker back cancels the suggestion, restores the line chart, shows panel options + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'), + 'time series to be rendered inside panel' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), + 'options pane to be rendered' + ).toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton), + 'discard changes button is still disabled since no changes were applied' + ).toBeDisabled(); + }); + + test('should not apply suggestion if you navigate back to the dashboard', async ({ + page, + selectors, + gotoPanelEditPage, + }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); + + // Verify we see suggestions + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')), + 'line chart suggestion to be rendered' + ).toBeVisible(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + await expect(page.getByRole('grid').getByRole('row').first(), 'table row to be rendered').toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton) + ).toBeDisabled(); + + // Verify that navigating back to the dashboard cancels the suggestion and restores the line chart. + await panelEditPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + page.locator('[data-viz-panel-key="panel-9"]').locator('.uplot'), + 'time series to be rendered inside the panel' + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts index d01447b7eb7..7872e5a69d0 100644 --- a/e2e-playwright/various-suite/visualization-suggestions.spec.ts +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e'; test.describe( 'Visualization suggestions', { - tag: ['@various'], + tag: ['@various', '@suggestions'], }, () => { test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => { diff --git a/e2e/cypress/plugins/typescriptPreprocessor.js b/e2e/cypress/plugins/typescriptPreprocessor.js index 27765380994..ed054f6f943 100644 --- a/e2e/cypress/plugins/typescriptPreprocessor.js +++ b/e2e/cypress/plugins/typescriptPreprocessor.js @@ -18,6 +18,7 @@ const webpackOptions = { }, resolve: { extensions: ['.ts', '.js'], + conditionNames: ['@grafana-app/source', '...'], }, }; diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 9ecdfaed054..597f2b94a1a 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -397,7 +397,7 @@ }, "packages/grafana-prometheus/src/language_provider.ts": { "@typescript-eslint/no-explicit-any": { - "count": 7 + "count": 2 } }, "packages/grafana-prometheus/src/language_utils.ts": { @@ -669,7 +669,7 @@ }, "packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx": { "no-restricted-syntax": { - "count": 3 + "count": 2 } }, "packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts": { @@ -763,11 +763,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/Select/resetSelectStyles.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Select/types.ts": { "@typescript-eslint/no-explicit-any": { "count": 6 @@ -786,11 +781,6 @@ "count": 13 } }, - "packages/grafana-ui/src/components/Sparkline/Sparkline.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 @@ -809,11 +799,6 @@ "count": 2 } }, - "packages/grafana-ui/src/components/Table/TableNG/utils.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/TableRT/Filter.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -886,11 +871,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/VizLegend/types.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -1447,22 +1427,6 @@ "count": 1 } }, - "public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - }, - "@typescript-eslint/no-explicit-any": { - "count": 2 - }, - "no-restricted-syntax": { - "count": 1 - } - }, - "public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/alerting/unified/components/receivers/form/CloudCommonChannelSettings.tsx": { "no-restricted-syntax": { "count": 1 @@ -1473,17 +1437,6 @@ "count": 1 } }, - "public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 2 - }, - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -1832,7 +1785,7 @@ }, "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": { "react-hooks/rules-of-hooks": { - "count": 4 + "count": 5 } }, "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": { @@ -1845,11 +1798,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/dashboard-scene/pages/DashboardScenePage.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 2 @@ -1940,11 +1888,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/serialization/angularMigration.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -1968,11 +1911,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/settings/JsonModelEditView.tsx": { - "react/no-unescaped-entities": { - "count": 2 - } - }, "public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx": { "react-hooks/rules-of-hooks": { "count": 4 @@ -2001,11 +1939,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/dashboard-scene/settings/variables/components/VariableTextField.tsx": { "no-restricted-syntax": { "count": 1 @@ -2062,9 +1995,6 @@ }, "@typescript-eslint/no-explicit-any": { "count": 3 - }, - "no-restricted-syntax": { - "count": 3 } }, "public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx": { @@ -2457,11 +2387,6 @@ "count": 1 } }, - "public/app/features/datasources/components/DataSourceLoadError.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/datasources/components/DataSourcePluginState.tsx": { "no-restricted-syntax": { "count": 3 @@ -2900,11 +2825,6 @@ "count": 1 } }, - "public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx": { - "@grafana/no-aria-label-selectors": { - "count": 1 - } - }, "public/app/features/panel/panellinks/linkSuppliers.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -2943,11 +2863,6 @@ "count": 1 } }, - "public/app/features/plugins/admin/components/PluginDetailsPage.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/plugins/admin/helpers.ts": { "no-restricted-syntax": { "count": 2 @@ -3145,11 +3060,6 @@ "count": 2 } }, - "public/app/features/teams/TeamGroupSync.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/templating/fieldAccessorCache.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -4326,9 +4236,6 @@ } }, "public/app/plugins/panel/geomap/components/DebugOverlay.tsx": { - "@grafana/no-aria-label-selectors": { - "count": 1 - }, "react-prefer-function-component/react-prefer-function-component": { "count": 1 } @@ -4422,7 +4329,7 @@ }, "public/app/plugins/panel/heatmap/utils.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 16 + "count": 14 } }, "public/app/plugins/panel/histogram/Histogram.tsx": { diff --git a/go.mod b/go.mod index e589e5a18a5..8768e51f86a 100644 --- a/go.mod +++ b/go.mod @@ -46,9 +46,9 @@ require ( github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // @grafana/grafana-backend-group github.com/bwmarrin/snowflake v0.3.0 // @grafana/grafana-app-platform-squad - github.com/centrifugal/centrifuge v0.37.2 // @grafana/grafana-app-platform-squad + github.com/centrifugal/centrifuge v0.38.0 // @grafana/grafana-app-platform-squad github.com/crewjam/saml v0.4.14 // @grafana/identity-access-team 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 @@ -87,7 +87,7 @@ require ( 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-20251204145817-de8c2bbf9eba // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -97,8 +97,8 @@ 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.48.5 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.7 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.48.7 // @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 @@ -157,7 +157,7 @@ require ( github.com/prometheus/alertmanager v0.28.2 // @grafana/alerting-backend github.com/prometheus/client_golang v1.23.2 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.2 // @grafana/grafana-backend-group - github.com/prometheus/common v0.67.3 // @grafana/alerting-backend + github.com/prometheus/common v0.67.4 // @grafana/alerting-backend github.com/prometheus/prometheus v0.303.1 // @grafana/alerting-backend github.com/prometheus/sigv4 v0.1.2 // @grafana/alerting-backend github.com/puzpuzpuz/xsync/v4 v4.2.0 // @grafana/grafana-backend-group @@ -166,7 +166,7 @@ require ( github.com/rs/cors v1.11.1 // @grafana/identity-access-team github.com/russellhaering/goxmldsig v1.4.0 // @grafana/grafana-backend-group github.com/shopspring/decimal v1.4.0 // @grafana/grafana-datasources-core-services - github.com/spf13/cobra v1.10.1 // @grafana/grafana-app-platform-squad + github.com/spf13/cobra v1.10.2 // @grafana/grafana-app-platform-squad 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 @@ -181,18 +181,19 @@ require ( 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.etcd.io/bbolt v1.4.2 // @grafana/grafana-search-and-storage 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/google.golang.org/grpc/otelgrpc v0.64.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 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // @grafana/sharing-squad go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel v1.38.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.39.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/sdk v1.38.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.38.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.39.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.39.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/mock v0.6.0 // @grafana/grafana-operator-experience-squad @@ -200,30 +201,30 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // @grafana/alerting-backend 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.45.0 // @grafana/grafana-backend-group - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // @grafana/alerting-backend - golang.org/x/mod v0.30.0 // indirect; @grafana/grafana-backend-group - golang.org/x/net v0.47.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.33.0 // @grafana/identity-access-team - golang.org/x/sync v0.18.0 // @grafana/alerting-backend - golang.org/x/text v0.31.0 // @grafana/grafana-backend-group + golang.org/x/crypto v0.46.0 // @grafana/grafana-backend-group + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // @grafana/alerting-backend + golang.org/x/mod v0.31.0 // indirect; @grafana/grafana-backend-group + golang.org/x/net v0.48.0 // @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/oauth2 v0.34.0 // @grafana/identity-access-team + golang.org/x/sync v0.19.0 // @grafana/alerting-backend + golang.org/x/text v0.32.0 // @grafana/grafana-backend-group golang.org/x/time v0.14.0 // @grafana/grafana-backend-group - golang.org/x/tools v0.39.0 // indirect; @grafana/grafana-as-code + golang.org/x/tools v0.40.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent google.golang.org/api v0.242.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.77.0 // @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.11 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group - k8s.io/api v0.34.2 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.34.2 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.34.2 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.34.2 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.34.2 // @grafana/grafana-app-platform-squad + k8s.io/api v0.34.3 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.34.3 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.34.3 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.34.3 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.34.3 // @grafana/grafana-app-platform-squad k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad - k8s.io/kube-aggregator v0.34.2 // @grafana/grafana-app-platform-squad - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // @grafana/grafana-app-platform-squad - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // @grafana/partner-datasources + k8s.io/kube-aggregator v0.34.3 // @grafana/grafana-app-platform-squad + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // @grafana/grafana-app-platform-squad + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // @grafana/partner-datasources modernc.org/sqlite v1.40.1 // @grafana/grafana-backend-group pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad @@ -250,6 +251,7 @@ require ( github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/preferences v0.0.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/apps/provisioning v0.0.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/scope v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/secret v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/shorturl v0.0.0 // @grafana/sharing-squad @@ -294,7 +296,7 @@ replace ( ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.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 @@ -302,7 +304,7 @@ require ( cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect - cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect + cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect @@ -385,7 +387,7 @@ require ( github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // @grafana/alerting-backend github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/centrifugal/protocol v0.16.2 // indirect + github.com/centrifugal/protocol v0.17.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect @@ -396,7 +398,7 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.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 @@ -412,7 +414,7 @@ require ( github.com/dolthub/maphash v0.1.0 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/proto v1.13.2 // indirect + github.com/emicklei/proto v1.14.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect @@ -426,10 +428,10 @@ require ( github.com/go-logr/stdr v1.2.2 // 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/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/spec v0.22.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/swag v0.25.4 // indirect github.com/go-openapi/validate v0.25.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect @@ -444,7 +446,7 @@ require ( 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.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-github/v64 v64.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect @@ -500,7 +502,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/magefile/mage v1.15.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/matryer/is v1.4.1 // indirect github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect @@ -558,10 +560,10 @@ require ( github.com/pressly/goose/v3 v3.26.0 // 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/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/redis/rueidis v1.0.64 // indirect + github.com/redis/rueidis v1.0.68 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -594,7 +596,7 @@ require ( github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect @@ -602,10 +604,9 @@ require ( github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.16.3 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.etcd.io/bbolt v1.4.2 // 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.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector/featuregate v1.44.0 // indirect @@ -617,29 +618,29 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.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.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 + go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/term v0.37.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/term v0.38.0 // 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-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect @@ -647,13 +648,13 @@ require ( 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/apiextensions-apiserver v0.34.2 // indirect - k8s.io/kms v0.34.2 // indirect + k8s.io/apiextensions-apiserver v0.34.3 // indirect + k8s.io/kms v0.34.3 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // 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/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) @@ -665,17 +666,19 @@ require ( 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/ebitengine/purego v0.8.2 // 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 - 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/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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 @@ -686,7 +689,8 @@ require ( 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/quagmt/udecimal v1.9.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.3 // 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 diff --git a/go.sum b/go.sum index 91104bfeaf4..ea251101dc8 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fc buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1/go.mod h1:1M7nlq2ljfzb95x9LaA2j1gYIvDkVZii58mGvTa9ExM= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= @@ -630,8 +630,8 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= -cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -982,8 +982,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 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= @@ -1006,10 +1006,10 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/centrifugal/centrifuge v0.37.2 h1:rerQNvDfYN2FZEkVtb/hvGV7SIrJfEQrKF3MaE8GDlo= -github.com/centrifugal/centrifuge v0.37.2/go.mod h1:aj4iRJGhzi3SlL8iUtVezxway1Xf8g+hmNQkLLO7sS8= -github.com/centrifugal/protocol v0.16.2 h1:KoIHgDeX1fFxyxQoKW+6E8ZTCf5mwGm8JyGoJ5NBMbQ= -github.com/centrifugal/protocol v0.16.2/go.mod h1:Q7OpS/8HMXDnL7f9DpNx24IhG96MP88WPpVTTCdrokI= +github.com/centrifugal/centrifuge v0.38.0 h1:UJTowwc5lSwnpvd3vbrTseODbU7osSggN67RTrJ8EfQ= +github.com/centrifugal/centrifuge v0.38.0/go.mod h1:rcZLARnO5GXOeE9qG7iIPMvERxESespqkSX4cGLCAzo= +github.com/centrifugal/protocol v0.17.0 h1:hD0WczyiG7zrVJcgkQsd5/nhfFXt0Y04SJHV2Z7B1rg= +github.com/centrifugal/protocol v0.17.0/go.mod h1:9MdiYyjw5Bw1+d5Sp4Y0NK+qiuTNyd88nrHJsUUh8k4= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -1064,8 +1064,9 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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= @@ -1142,8 +1143,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m 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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/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= @@ -1156,8 +1157,8 @@ github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVo github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 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/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= 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= @@ -1287,16 +1288,16 @@ github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwds github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.20.1/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM= github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -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/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= @@ -1336,28 +1337,36 @@ github.com/go-openapi/swag v0.19.4/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= github.com/go-openapi/swag v0.22.6/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.22.4/go.mod h1:qm6O8ZIcPVdSY5219468Jv7kBdGvkiZLPOmqnqTUZ2A= @@ -1490,8 +1499,8 @@ github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PU github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -1613,8 +1622,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= -github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts= +github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= 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-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -1635,10 +1644,10 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl 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.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= -github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= -github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo= +github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -1653,6 +1662,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= 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/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= 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= @@ -1977,8 +1988,8 @@ github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -2297,8 +2308,8 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= @@ -2319,24 +2330,26 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/prometheus v0.0.0-20190818123050-43acd0e2e93f/go.mod h1:rMTlmxGCvukf2KMu3fClMDKLLoJ5hl61MhcJ7xKakf0= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= 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/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0= github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/quagmt/udecimal v1.9.0 h1:TLuZiFeg0HhS6X8VDa78Y6XTaitZZfh+z5q4SXMzpDQ= +github.com/quagmt/udecimal v1.9.0/go.mod h1:ScmJ/xTGZcEoYiyMMzgDLn79PEJHcMBiJ4NNRT3FirA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 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/redis/rueidis v1.0.64 h1:XqgbueDuNV3qFdVdQwAHJl1uNt90zUuAJuzqjH4cw6Y= -github.com/redis/rueidis v1.0.64/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= +github.com/redis/rueidis v1.0.68 h1:gept0E45JGxVigWb3zoWHvxEc4IOC7kc4V/4XvN8eG8= +github.com/redis/rueidis v1.0.68/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -2393,8 +2406,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/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= 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= @@ -2442,8 +2455,8 @@ github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155 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 v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2542,8 +2555,8 @@ github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2th github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= 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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= @@ -2597,15 +2610,15 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2 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.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= -go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -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/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= 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.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= -go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= 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= @@ -2644,14 +2657,14 @@ go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= 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= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= 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.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= -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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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= @@ -2659,8 +2672,8 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bn go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -2671,12 +2684,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9 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/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= 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= @@ -2690,23 +2703,23 @@ go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCf go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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 v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 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/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= -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/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -2784,8 +2797,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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= @@ -2801,8 +2814,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 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-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -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/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 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= @@ -2848,8 +2861,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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -2935,8 +2948,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -2974,8 +2987,8 @@ golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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= @@ -2999,8 +3012,8 @@ golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -3134,11 +3147,11 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.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-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 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= @@ -3157,8 +3170,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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -3181,8 +3194,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -3267,8 +3280,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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -3517,15 +3530,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. 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= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 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= @@ -3596,8 +3609,8 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -3608,8 +3621,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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= @@ -3659,19 +3672,19 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= +k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -3679,17 +3692,17 @@ k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 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.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= -k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-aggregator v0.34.2 h1:Nn0Vksj67WHBL2x7bJ6vuxL44RbMTK6uRtXX+3vMVJk= -k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q= +k8s.io/kms v0.34.3 h1:QzBOD0sk1bGQVMcZQAHGjtbP1iKZJUyhC6D0I+BTxIE= +k8s.io/kms v0.34.3/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-aggregator v0.34.3 h1:rKsZWTD2As4dKuv+zzdJU0uo5H7bFlAEoSucai4mW6M= +k8s.io/kube-aggregator v0.34.3/go.mod h1:d4D8PV2FK4Qlq6u442FSum1tHPhK9tKdKBfH/A3R0I0= k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= k8s.io/kube-openapi v0.0.0-20190722073852-5e22f3d471e6/go.mod h1:RZvgC8MSN6DjiMV6oIfEE9pDL9CYXokkfaCKZeHm3nc= -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/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= @@ -3756,10 +3769,10 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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 v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= diff --git a/go.work b/go.work index ecfab4e96ce..884f393673e 100644 --- a/go.work +++ b/go.work @@ -23,6 +23,7 @@ use ( ./apps/plugins ./apps/preferences ./apps/provisioning + ./apps/quotas ./apps/scope ./apps/secret ./apps/shorturl diff --git a/go.work.sum b/go.work.sum index 3017d8eb878..ca22b546c86 100644 --- a/go.work.sum +++ b/go.work.sum @@ -13,6 +13,7 @@ cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.19.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= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 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 v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= @@ -329,6 +330,8 @@ github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeN github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -410,6 +413,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye 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/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= 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/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= @@ -489,6 +493,8 @@ github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp 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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 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= @@ -517,6 +523,7 @@ github.com/bsm/sarama-cluster v2.1.13+incompatible h1:bqU3gMJbWZVxLZ9PGWVKP05yOm github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= github.com/bufbuild/protovalidate-go v0.2.1 h1:pJr07sYhliyfj/STAM7hU4J3FKpVeLVKvOBmOTN8j+s= github.com/bufbuild/protovalidate-go v0.2.1/go.mod h1:e7XXDtlxj5vlEyAgsrxpzayp4cEMKCSSb8ZCkin+MVA= +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= @@ -526,6 +533,12 @@ 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/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/centrifugal/centrifuge v0.37.2/go.mod h1:aj4iRJGhzi3SlL8iUtVezxway1Xf8g+hmNQkLLO7sS8= +github.com/centrifugal/protocol v0.16.2/go.mod h1:Q7OpS/8HMXDnL7f9DpNx24IhG96MP88WPpVTTCdrokI= 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= @@ -551,6 +564,7 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1Ig github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/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= @@ -558,6 +572,7 @@ 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/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 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= @@ -680,8 +695,7 @@ github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6 github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/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/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= @@ -710,8 +724,11 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= 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/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo= +github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= 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.17.6/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= @@ -761,6 +778,7 @@ 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-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= @@ -775,6 +793,15 @@ github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5 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/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= 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= @@ -797,6 +824,7 @@ github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFx 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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 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= @@ -856,10 +884,13 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= +github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= 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/authlib/types v0.0.0-20250926065801-df98203cff37/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/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cog v0.0.43/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= @@ -873,7 +904,6 @@ github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0U 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 v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= 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= @@ -887,9 +917,10 @@ github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5 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/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.48.2 h1:tI+a9slUvxKUgweXDzUqkca2LWV3g1UdaSvwt8nQNHg= github.com/grafana/grafana-app-sdk/logging v0.48.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.5 h1:vWiTZrsSscbC5IQq2heWXm0dXhvg40nXIeUTCdE9qsc= +github.com/grafana/grafana-app-sdk/logging v0.48.5/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= @@ -908,6 +939,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI 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-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= 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= @@ -926,6 +958,7 @@ github.com/grafana/grafana/apps/preferences v0.0.0-20250805120145-0c5a00302924 h github.com/grafana/grafana/apps/preferences v0.0.0-20250805120145-0c5a00302924/go.mod h1:NQlHMO5fHhjexw71wVjv522532NRvFg5F4tcjUEktjs= github.com/grafana/grafana/apps/preferences v0.0.0-20250805123034-066163d71001 h1:y2AHkdji2I+zXv8rsSC8OjWEzJJjqW5OlmCsZR5+RuU= github.com/grafana/grafana/apps/preferences v0.0.0-20250805123034-066163d71001/go.mod h1:NQlHMO5fHhjexw71wVjv522532NRvFg5F4tcjUEktjs= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= @@ -951,12 +984,12 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= 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/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= 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-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 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= @@ -1332,6 +1365,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgc github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 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= @@ -1346,6 +1380,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq 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 v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= 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= @@ -1368,6 +1403,7 @@ 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/rueidis v1.0.64/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 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= @@ -1375,7 +1411,6 @@ github.com/richardartoul/molecule v1.0.0/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+u github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/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= @@ -1387,6 +1422,8 @@ github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/ github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 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= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ= @@ -1400,6 +1437,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs= github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= +github.com/segmentio/asm v1.1.4/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= 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= @@ -1412,8 +1450,6 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl 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/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= @@ -1593,7 +1629,6 @@ go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5queth go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 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/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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= @@ -1809,6 +1844,7 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1: 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/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 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= @@ -1880,10 +1916,10 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 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.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= 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/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= @@ -1925,6 +1961,7 @@ golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 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.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 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= @@ -1936,12 +1973,14 @@ 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/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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-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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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= @@ -2008,6 +2047,7 @@ 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/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.12.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= @@ -2028,9 +2068,11 @@ golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 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.1/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= @@ -2081,11 +2123,12 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go. 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/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= 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-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 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= @@ -2112,12 +2155,15 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go. 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/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 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= @@ -2135,7 +2181,6 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E 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 v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 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= @@ -2179,30 +2224,37 @@ k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= 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/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8= k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= 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/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= -k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= -k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0= -k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I= +k8s.io/code-generator v0.34.3 h1:6ipJKsJZZ9q21BO8I2jEj4OLN3y8/1n4aihKN0xKmQk= +k8s.io/code-generator v0.34.3/go.mod h1:oW73UPYpGLsbRN8Ozkhd6ZzkF8hzFCiYmvEuWZDroI4= k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs= k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= 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-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= 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.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= 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= @@ -2250,7 +2302,6 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih 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/structured-merge-diff/v6 v6.3.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= diff --git a/hack/go.mod b/hack/go.mod index e9ecfdc60b4..57179bc0550 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -2,7 +2,7 @@ module github.com/grafana/grafana/hack go 1.25.5 -require k8s.io/code-generator v0.34.2 +require k8s.io/code-generator v0.34.3 require ( github.com/go-logr/logr v1.4.2 // indirect diff --git a/hack/go.sum b/hack/go.sum index 25c035d777a..b427dbd73ef 100644 --- a/hack/go.sum +++ b/hack/go.sum @@ -12,8 +12,8 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0= -k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I= +k8s.io/code-generator v0.34.3 h1:6ipJKsJZZ9q21BO8I2jEj4OLN3y8/1n4aihKN0xKmQk= +k8s.io/code-generator v0.34.3/go.mod h1:oW73UPYpGLsbRN8Ozkhd6ZzkF8hzFCiYmvEuWZDroI4= 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/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 59976c9bf3c..03dd95943a2 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -11,7 +11,7 @@ set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. pushd "${SCRIPT_ROOT}/hack" && GO111MODULE=on go mod tidy && popd -CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.34.2)} +CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.34.3)} OUTDIR="${HOME}/go/src" OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions.list" diff --git a/jest.config.js b/jest.config.js index ef107c8cc24..17a2ce9ca32 100644 --- a/jest.config.js +++ b/jest.config.js @@ -40,6 +40,9 @@ const esModules = [ module.exports = { verbose: false, testEnvironment: 'jsdom', + testEnvironmentOptions: { + customExportConditions: ['@grafana-app/source', 'browser'], + }, transform: { '^.+\\.(ts|tsx|js|jsx)$': [require.resolve('ts-jest')], }, diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 346edbb4753..08b7a1b2b6e 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -218,6 +218,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -245,6 +247,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting @@ -696,6 +702,10 @@ lineage: schemas: [{ // Field options allow you to change how the data is displayed in your visualizations. fieldConfig?: #FieldConfigSource + + // When a panel is migrated from a previous version (Angular to React), this field is set to the original panel type. + // This is used to determine the original panel type when migrating to a new version so the plugin migration can be applied. + autoMigrateFrom?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. diff --git a/package.json b/package.json index 2a0fec2a286..4750d1e220f 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,10 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", - "e2e:playwright": "yarn playwright test --grep-invert @cloud-plugins", - "e2e:playwright:cloud-plugins": "yarn playwright test --grep @cloud-plugins", - "e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts", - "e2e:acceptance": "yarn playwright test --grep @acceptance", + "e2e:playwright": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep-invert @cloud-plugins", + "e2e:playwright:cloud-plugins": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @cloud-plugins", + "e2e:playwright:storybook": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test -c playwright.storybook.config.ts", + "e2e:acceptance": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @acceptance", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", @@ -63,7 +63,7 @@ "storybook": "yarn workspace @grafana/ui storybook --ci", "storybook:build": "yarn workspace @grafana/ui storybook:build", "themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json", - "themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --platform=node --tsconfig=./scripts/cli/tsconfig.json | node", + "themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node", "themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'", "typecheck": "tsc --noEmit && yarn run packages:typecheck", "plugins:build-bundled": "echo 'bundled plugins are no longer supported'", @@ -93,7 +93,7 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "8.2.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "^3.0.3", + "@grafana/plugin-e2e": "^3.1.0", "@grafana/test-utils": "workspace:*", "@manypkg/get-packages": "^3.0.0", "@npmcli/package-json": "^6.0.0", @@ -125,7 +125,6 @@ "@types/eslint": "9.6.1", "@types/eslint-scope": "^8.0.0", "@types/file-saver": "2.0.7", - "@types/glob": "^9.0.0", "@types/google.analytics": "^0.0.46", "@types/gtag.js": "^0.0.20", "@types/history": "4.7.11", @@ -291,14 +290,14 @@ "@grafana/google-sdk": "0.3.5", "@grafana/i18n": "workspace:*", "@grafana/lezer-logql": "0.2.9", - "@grafana/llm": "0.22.1", + "@grafana/llm": "1.0.1", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/plugin-ui": "^0.11.1", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "6.47.1", - "@grafana/scenes-react": "6.47.1", + "@grafana/scenes": "6.52.0", + "@grafana/scenes-react": "6.52.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -461,7 +460,9 @@ "gitconfiglocal": "2.1.0", "tmp@npm:^0.0.33": "~0.2.1", "js-yaml@npm:4.1.0": "^4.1.0", - "js-yaml@npm:=4.1.0": "^4.1.0" + "js-yaml@npm:=4.1.0": "^4.1.0", + "nodemailer": "7.0.7", + "@storybook/core@npm:8.6.2": "patch:@storybook/core@npm%3A8.6.2#~/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch" }, "workspaces": { "packages": [ diff --git a/packages/README.md b/packages/README.md index b42f7082c5c..3fa0dceebc7 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,13 +2,32 @@ ## Exporting code conventions -`@grafana/ui`, `@grafana/data` and `@grafana/runtime` makes use of `exports` in package.json to define three entrypoints that Grafana core and Grafana plugins can access. Before exposing anything in these packages please consider the table below to better understand the use case of each export. +All the `@grafana` packages in this repo (except `@grafana/schema`) make use of `exports` in package.json to define entrypoints that Grafana core and Grafana plugins can access. Exports can also be used to restrict access to internal files in packages. -| Export Name | Import Path | Description | Available to Grafana | Available to plugins | -| ------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------- | -| `./` | `@grafana/ui` | The public API entrypoint. If the code is stable and you want to share it everywhere, this is the place to export it. | ✅ | ✅ | -| `./unstable` | `@grafana/ui/unstable` | The public API entrypoint for all experimental code. If you want to iterate and test code from Grafana and plugins, this is the place to export it. | ✅ | ✅ | -| `./internal` | `@grafana/ui/internal` | The private API entrypoint for internal code shared with Grafana. If you need to import code in Grafana but don't want to expose it to plugins, this is the place to export it. | ✅ | ❌ | +Package authors are free to create as many exports as they like but should consider the following points: + +1. Resolution of source code within this repo is handled by the [customCondition](https://www.typescriptlang.org/tsconfig/#customConditions) `@grafana-app/source`. This allows the frontend tooling in this repo to resolve to the source code preventing the need to build all the packages up front. When adding exports it is important to add an entry for the custom condition as the first item. All other entries should point to the built, bundled files. For example: + + ```json + "exports": { + ".": { + "@grafana-app/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + } + ``` + +2. If you add exports to your package you must export the `package.json` file. + +3. Before exposing anything in these packages please consider the table below to better understand the conventions we have put in place for most of the packages in this repository. + +| Export Name | Import Path | Description | Available to Grafana | Available to plugins | +| ------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------- | +| `./` | `@grafana/ui` | The public API entrypoint. If the code is stable and you want to share it everywhere, this is the place to export it. | ✅ | ✅ | +| `./unstable` | `@grafana/ui/unstable` | The public API entrypoint for all experimental code. If you want to iterate and test code from Grafana and plugins, this is the place to export it. | ✅ | ✅ | +| `./internal` | `@grafana/ui/internal` | The private API entrypoint for internal code shared with Grafana. If you want to co-locate code in a package with it's public API but only want the Grafana application to access it, this is the place to export it. | ✅ | ❌ | ## Versioning diff --git a/packages/grafana-alerting/package.json b/packages/grafana-alerting/package.json index 1c3ab2bf9b4..d64f1c01893 100644 --- a/packages/grafana-alerting/package.json +++ b/packages/grafana-alerting/package.json @@ -17,32 +17,34 @@ "url": "http://github.com/grafana/grafana.git", "directory": "packages/grafana-alerting" }, - "main": "src/index.ts", - "types": "src/index.ts", - "module": "src/index.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/types/index.d.ts", "exports": { "./package.json": "./package.json", ".": { - "import": "./src/index.ts", - "require": "./src/index.ts" - }, - "./internal": { - "import": "./src/internal.ts", - "require": "./src/internal.ts" + "@grafana-app/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" }, "./unstable": { - "import": "./src/unstable.ts", - "require": "./src/unstable.ts" + "@grafana-app/source": "./src/unstable.ts", + "types": "./dist/types/unstable.d.ts", + "import": "./dist/esm/unstable.mjs", + "require": "./dist/cjs/unstable.cjs" + }, + "./internal": { + "@grafana-app/source": "./src/internal.ts" }, "./testing": { - "import": "./src/testing.ts", - "require": "./src/testing.ts" + "@grafana-app/source": "./src/testing.ts", + "types": "./dist/types/testing.d.ts", + "import": "./dist/esm/testing.mjs", + "require": "./dist/cjs/testing.cjs" } }, "publishConfig": { - "main": "./dist/cjs/index.cjs", - "module": "./dist/esm/index.mjs", - "types": "./dist/types/index.d.ts", "access": "public" }, "files": [ @@ -57,8 +59,8 @@ "clean": "rimraf ./dist ./compiled ./unstable ./testing ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "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", + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", + "postpack": "mv package.json.bak package.json", "i18n-extract": "i18next-cli extract --sync-primary" }, "devDependencies": { diff --git a/packages/grafana-alerting/rollup.config.ts b/packages/grafana-alerting/rollup.config.ts index f8f41e3ad2c..43914b47b70 100644 --- a/packages/grafana-alerting/rollup.config.ts +++ b/packages/grafana-alerting/rollup.config.ts @@ -9,19 +9,19 @@ export default [ { input: entryPoint, plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')], + output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')], treeshake: false, }, { input: 'src/unstable.ts', plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')], + output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')], treeshake: false, }, { input: 'src/testing.ts', plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')], + output: [cjsOutput(pkg, 'grafana-alerting'), esmOutput(pkg, 'grafana-alerting')], treeshake: false, }, ]; diff --git a/packages/grafana-api-clients/package.json b/packages/grafana-api-clients/package.json index 47704b83e0e..f1feef1005e 100644 --- a/packages/grafana-api-clients/package.json +++ b/packages/grafana-api-clients/package.json @@ -15,88 +15,121 @@ "url": "https://github.com/grafana/grafana.git", "directory": "packages/grafana-api-clients" }, - "main": "src/index.ts", - "module": "src/index.ts", - "types": "src/index.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/types/index.d.ts", "exports": { "./package.json": "./package.json", ".": { - "import": "./src/index.ts", - "require": "./src/index.ts" + "@grafana-app/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" }, "./rtkq": { - "import": "./src/clients/rtkq/index.ts", - "require": "./src/clients/rtkq/index.ts" + "@grafana-app/source": "./src/clients/rtkq/index.ts", + "types": "./dist/types/clients/rtkq/index.d.ts", + "import": "./dist/esm/clients/rtkq/index.mjs", + "require": "./dist/cjs/clients/rtkq/index.cjs" }, "./rtkq/advisor/v0alpha1": { - "import": "./src/clients/rtkq/advisor/v0alpha1/index.ts", - "require": "./src/clients/rtkq/advisor/v0alpha1/index.ts" - }, - "./rtkq/correlations/v0alpha1": { - "import": "./src/clients/rtkq/correlations/v0alpha1/index.ts", - "require": "./src/clients/rtkq/correlations/v0alpha1/index.ts" - }, - "./rtkq/dashboard/v0alpha1": { - "import": "./src/clients/rtkq/dashboard/v0alpha1/index.ts", - "require": "./src/clients/rtkq/dashboard/v0alpha1/index.ts" - }, - "./rtkq/folder/v1beta1": { - "import": "./src/clients/rtkq/folder/v1beta1/index.ts", - "require": "./src/clients/rtkq/folder/v1beta1/index.ts" - }, - "./rtkq/iam/v0alpha1": { - "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" - }, - "./rtkq/legacy/preferences": { - "import": "./src/clients/rtkq/preferences/user/index.ts", - "require": "./src/clients/rtkq/preferences/user/index.ts" - }, - "./rtkq/legacy/user": { - "import": "./src/clients/rtkq/user/index.ts", - "require": "./src/clients/rtkq/user/index.ts" - }, - "./rtkq/playlist/v0alpha1": { - "import": "./src/clients/rtkq/playlist/v0alpha1/index.ts", - "require": "./src/clients/rtkq/playlist/v0alpha1/index.ts" - }, - "./rtkq/preferences/v1alpha1": { - "import": "./src/clients/rtkq/preferences/v1alpha1/index.ts", - "require": "./src/clients/rtkq/preferences/v1alpha1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/advisor/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/advisor/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/advisor/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/advisor/v0alpha1/index.cjs" }, "./rtkq/collections/v1alpha1": { - "import": "./src/clients/rtkq/collections/v1alpha1/index.ts", - "require": "./src/clients/rtkq/collections/v1alpha1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/collections/v1alpha1/index.ts", + "types": "./dist/types/clients/rtkq/collections/v1alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/collections/v1alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/collections/v1alpha1/index.cjs" + }, + "./rtkq/correlations/v0alpha1": { + "@grafana-app/source": "./src/clients/rtkq/correlations/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/correlations/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/correlations/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/correlations/v0alpha1/index.cjs" + }, + "./rtkq/dashboard/v0alpha1": { + "@grafana-app/source": "./src/clients/rtkq/dashboard/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/dashboard/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/dashboard/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/dashboard/v0alpha1/index.cjs" + }, + "./rtkq/folder/v1beta1": { + "@grafana-app/source": "./src/clients/rtkq/folder/v1beta1/index.ts", + "types": "./dist/types/clients/rtkq/folder/v1beta1/index.d.ts", + "import": "./dist/esm/clients/rtkq/folder/v1beta1/index.mjs", + "require": "./dist/cjs/clients/rtkq/folder/v1beta1/index.cjs" + }, + "./rtkq/iam/v0alpha1": { + "@grafana-app/source": "./src/clients/rtkq/iam/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/iam/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/iam/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/iam/v0alpha1/index.cjs" + }, + "./rtkq/legacy": { + "@grafana-app/source": "./src/clients/rtkq/legacy/index.ts", + "types": "./dist/types/clients/rtkq/legacy/index.d.ts", + "import": "./dist/esm/clients/rtkq/legacy/index.mjs", + "require": "./dist/cjs/clients/rtkq/legacy/index.cjs" + }, + "./rtkq/legacy/migrate-to-cloud": { + "@grafana-app/source": "./src/clients/rtkq/migrate-to-cloud/index.ts", + "types": "./dist/types/clients/rtkq/migrate-to-cloud/index.d.ts", + "import": "./dist/esm/clients/rtkq/migrate-to-cloud/index.mjs", + "require": "./dist/cjs/clients/rtkq/migrate-to-cloud/index.cjs" + }, + "./rtkq/legacy/preferences": { + "@grafana-app/source": "./src/clients/rtkq/preferences/user/index.ts", + "types": "./dist/types/clients/rtkq/preferences/user/index.d.ts", + "import": "./dist/esm/clients/rtkq/preferences/user/index.mjs", + "require": "./dist/cjs/clients/rtkq/preferences/user/index.cjs" + }, + "./rtkq/legacy/user": { + "@grafana-app/source": "./src/clients/rtkq/user/index.ts", + "types": "./dist/types/clients/rtkq/user/index.d.ts", + "import": "./dist/esm/clients/rtkq/user/index.mjs", + "require": "./dist/cjs/clients/rtkq/user/index.cjs" + }, + "./rtkq/playlist/v0alpha1": { + "@grafana-app/source": "./src/clients/rtkq/playlist/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/playlist/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/playlist/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/playlist/v0alpha1/index.cjs" + }, + "./rtkq/preferences/v1alpha1": { + "@grafana-app/source": "./src/clients/rtkq/preferences/v1alpha1/index.ts", + "types": "./dist/types/clients/rtkq/preferences/v1alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/preferences/v1alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/preferences/v1alpha1/index.cjs" }, "./rtkq/provisioning/v0alpha1": { - "import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts", - "require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/provisioning/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/provisioning/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/provisioning/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/provisioning/v0alpha1/index.cjs" }, "./rtkq/shorturl/v1beta1": { - "import": "./src/clients/rtkq/shorturl/v1beta1/index.ts", - "require": "./src/clients/rtkq/shorturl/v1beta1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/shorturl/v1beta1/index.ts", + "types": "./dist/types/clients/rtkq/shorturl/v1beta1/index.d.ts", + "import": "./dist/esm/clients/rtkq/shorturl/v1beta1/index.mjs", + "require": "./dist/cjs/clients/rtkq/shorturl/v1beta1/index.cjs" }, "./rtkq/historian.alerting/v0alpha1": { - "import": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts", - "require": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/historian.alerting/v0alpha1/index.ts", + "types": "./dist/types/clients/rtkq/historian.alerting/v0alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/historian.alerting/v0alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/historian.alerting/v0alpha1/index.cjs" }, "./rtkq/logsdrilldown/v1alpha1": { - "import": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts", - "require": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts" + "@grafana-app/source": "./src/clients/rtkq/logsdrilldown/v1alpha1/index.ts", + "types": "./dist/types/clients/rtkq/logsdrilldown/v1alpha1/index.d.ts", + "import": "./dist/esm/clients/rtkq/logsdrilldown/v1alpha1/index.mjs", + "require": "./dist/cjs/clients/rtkq/logsdrilldown/v1alpha1/index.cjs" } }, "publishConfig": { - "main": "./dist/cjs/index.cjs", - "module": "./dist/esm/index.mjs", - "types": "./dist/types/index.d.ts", "access": "public" }, "files": [ diff --git a/packages/grafana-api-clients/rollup.config.ts b/packages/grafana-api-clients/rollup.config.ts index b36d59278fc..976a0c000fa 100644 --- a/packages/grafana-api-clients/rollup.config.ts +++ b/packages/grafana-api-clients/rollup.config.ts @@ -5,35 +5,17 @@ import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.part const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); -const apiClients = Object.entries<{ import: string; require: string }>(pkg.exports).filter(([key]) => - key.startsWith('./rtkq/') -); - -const apiClientConfigs = apiClients.map(([name, { import: importPath }]) => { - const baseCjsOutput = cjsOutput(pkg); - const entryFileNames = name.replace('./', '') + '.cjs'; - const cjsOutputConfig = { ...baseCjsOutput, entryFileNames }; - return { - input: importPath.replace('./', ''), - - plugins, - output: [cjsOutputConfig, esmOutput(pkg, 'grafana-api-clients')], - treeshake: false, - }; -}); - export default [ { input: entryPoint, plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')], + output: [cjsOutput(pkg, 'grafana-api-clients'), esmOutput(pkg, 'grafana-api-clients')], treeshake: false, }, { input: 'src/clients/rtkq/index.ts', plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')], + output: [cjsOutput(pkg, 'grafana-api-clients'), esmOutput(pkg, 'grafana-api-clients')], treeshake: false, }, - ...apiClientConfigs, ]; 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 5d3e72b13aa..326b53ccedd 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 @@ -243,6 +243,7 @@ const injectedRtkApi = api type: queryArg['type'], folder: queryArg.folder, facet: queryArg.facet, + facetLimit: queryArg.facetLimit, tags: queryArg.tags, libraryPanel: queryArg.libraryPanel, permission: queryArg.permission, @@ -284,6 +285,10 @@ const injectedRtkApi = api query: (queryArg) => ({ url: `/snapshots/delete/${queryArg.deleteKey}`, method: 'DELETE' }), invalidatesTags: ['Snapshot'], }), + getSnapshotSettings: build.query({ + query: () => ({ url: `/snapshots/settings` }), + providesTags: ['Snapshot'], + }), getSnapshot: build.query({ query: (queryArg) => ({ url: `/snapshots/${queryArg.name}`, @@ -663,6 +668,8 @@ export type SearchDashboardsAndFoldersApiArg = { folder?: string; /** count distinct terms for selected fields */ facet?: string[]; + /** maximum number of terms to return per facet (default 50, max 1000) */ + facetLimit?: number; /** tag query filter */ tags?: string[]; /** find dashboards that reference a given libraryPanel */ @@ -739,6 +746,8 @@ export type DeleteWithKeyApiArg = { /** unique key returned in create */ deleteKey: string; }; +export type GetSnapshotSettingsApiResponse = /** status 200 undefined */ any; +export type GetSnapshotSettingsApiArg = void; export type GetSnapshotApiResponse = /** status 200 OK */ Snapshot; export type GetSnapshotApiArg = { /** name of the Snapshot */ @@ -1270,6 +1279,8 @@ export const { useLazyListSnapshotQuery, useCreateSnapshotMutation, useDeleteWithKeyMutation, + useGetSnapshotSettingsQuery, + useLazyGetSnapshotSettingsQuery, useGetSnapshotQuery, useLazyGetSnapshotQuery, useDeleteSnapshotMutation, 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 8b0690f57ba..790db19a6e3 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 @@ -165,6 +165,19 @@ const injectedRtkApi = api }), providesTags: ['Search'], }), + getSearchUsers: build.query({ + query: (queryArg) => ({ + url: `/searchUsers`, + params: { + query: queryArg.query, + limit: queryArg.limit, + page: queryArg.page, + offset: queryArg.offset, + sort: queryArg.sort, + }, + }), + providesTags: ['Search'], + }), listServiceAccount: build.query({ query: (queryArg) => ({ url: `/serviceaccounts`, @@ -896,6 +909,18 @@ export type GetSearchTeamsApiArg = { /** page number to start from */ page?: number; }; +export type GetSearchUsersApiResponse = unknown; +export type GetSearchUsersApiArg = { + query?: string; + /** number of results to return */ + limit?: number; + /** page number (starting from 1) */ + page?: number; + /** number of results to skip */ + offset?: number; + /** sortable field */ + sort?: string; +}; 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). */ @@ -2067,6 +2092,9 @@ export type UserSpec = { role: string; title: string; }; +export type UserStatus = { + lastSeenAt: number; +}; 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; @@ -2075,6 +2103,7 @@ export type User = { metadata: ObjectMeta; /** Spec is the spec of the User */ spec: UserSpec; + status: UserStatus; }; 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 */ @@ -2120,6 +2149,8 @@ export const { useUpdateExternalGroupMappingMutation, useGetSearchTeamsQuery, useLazyGetSearchTeamsQuery, + useGetSearchUsersQuery, + useLazyGetSearchUsersQuery, useListServiceAccountQuery, useLazyListServiceAccountQuery, useCreateServiceAccountMutation, 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 index 5ba0b811289..4f17bb46ecf 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -658,10 +658,6 @@ const injectedRtkApi = api 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'], @@ -2574,8 +2570,6 @@ export type PostDashboardApiResponse = /** status 200 (empty) */ { 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 = { @@ -3446,16 +3440,16 @@ export type RemoveTeamGroupApiQueryApiResponse = /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; export type RemoveTeamGroupApiQueryApiArg = { groupId?: string; - teamId: number; + teamId: string; }; export type GetTeamGroupsApiApiResponse = /** status 200 (empty) */ TeamGroupDto[]; export type GetTeamGroupsApiApiArg = { - teamId: number; + teamId: string; }; export type AddTeamGroupApiApiResponse = /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; export type AddTeamGroupApiApiArg = { - teamId: number; + teamId: string; teamGroupMapping: TeamGroupMapping; }; export type SearchTeamGroupsApiResponse = /** status 200 (empty) */ SearchTeamGroupsQueryResult; @@ -4399,51 +4393,6 @@ export type SaveDashboardCommand = { 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; @@ -4535,6 +4484,45 @@ export type PublicDashboardDto = { timeSelectionEnabled?: boolean; uid?: string; }; +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 DashboardFullWithMeta = { dashboard?: Json; meta?: DashboardMeta; @@ -6619,8 +6607,6 @@ export const { useSearchDashboardSnapshotsQuery, useLazySearchDashboardSnapshotsQuery, usePostDashboardMutation, - useGetHomeDashboardQuery, - useLazyGetHomeDashboardQuery, useImportDashboardMutation, useInterpolateDashboardMutation, useListPublicDashboardsQuery, diff --git a/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts index 835194b59e5..22464a2b3fa 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts @@ -1,5 +1,10 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'LogsDrilldownDefaults', 'LogsDrilldown'] as const; +export const addTagTypes = [ + 'API Discovery', + 'LogsDrilldownDefaultColumns', + 'LogsDrilldownDefaults', + 'LogsDrilldown', +] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -10,6 +15,183 @@ const injectedRtkApi = api query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), + listLogsDrilldownDefaultColumns: build.query< + ListLogsDrilldownDefaultColumnsApiResponse, + ListLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + 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: ['LogsDrilldownDefaultColumns'], + }), + createLogsDrilldownDefaultColumns: build.mutation< + CreateLogsDrilldownDefaultColumnsApiResponse, + CreateLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + method: 'POST', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + deletecollectionLogsDrilldownDefaultColumns: build.mutation< + DeletecollectionLogsDrilldownDefaultColumnsApiResponse, + DeletecollectionLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + 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: ['LogsDrilldownDefaultColumns'], + }), + getLogsDrilldownDefaultColumns: build.query< + GetLogsDrilldownDefaultColumnsApiResponse, + GetLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['LogsDrilldownDefaultColumns'], + }), + replaceLogsDrilldownDefaultColumns: build.mutation< + ReplaceLogsDrilldownDefaultColumnsApiResponse, + ReplaceLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'PUT', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + deleteLogsDrilldownDefaultColumns: build.mutation< + DeleteLogsDrilldownDefaultColumnsApiResponse, + DeleteLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + updateLogsDrilldownDefaultColumns: build.mutation< + UpdateLogsDrilldownDefaultColumnsApiResponse, + UpdateLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + getLogsDrilldownDefaultColumnsStatus: build.query< + GetLogsDrilldownDefaultColumnsStatusApiResponse, + GetLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['LogsDrilldownDefaultColumns'], + }), + replaceLogsDrilldownDefaultColumnsStatus: build.mutation< + ReplaceLogsDrilldownDefaultColumnsStatusApiResponse, + ReplaceLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + updateLogsDrilldownDefaultColumnsStatus: build.mutation< + UpdateLogsDrilldownDefaultColumnsStatusApiResponse, + UpdateLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${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: ['LogsDrilldownDefaultColumns'], + }), listLogsDrilldownDefaults: build.query({ query: (queryArg) => ({ url: `/logsdrilldowndefaults`, @@ -340,6 +522,218 @@ const injectedRtkApi = api export { injectedRtkApi as generatedAPI }; export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; export type GetApiResourcesApiArg = void; +export type ListLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumnsList; +export type ListLogsDrilldownDefaultColumnsApiArg = { + /** 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 CreateLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns + | /** status 202 Accepted */ LogsDrilldownDefaultColumns; +export type CreateLogsDrilldownDefaultColumnsApiArg = { + /** 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; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type DeletecollectionLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ Status; +export type DeletecollectionLogsDrilldownDefaultColumnsApiArg = { + /** 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 GetLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumns; +export type GetLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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 ReplaceLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type ReplaceLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type DeleteLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | Status + | /** status 202 Accepted */ Status; +export type DeleteLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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 UpdateLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type UpdateLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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 GetLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumns; +export type GetLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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 ReplaceLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type ReplaceLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type UpdateLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type UpdateLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + 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 ListLogsDrilldownDefaultsApiResponse = /** status 200 OK */ LogsDrilldownDefaultsList; export type ListLogsDrilldownDefaultsApiArg = { /** 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). */ @@ -874,13 +1268,21 @@ export type ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; -export type LogsDrilldownDefaultsSpec = { - defaultFields: string[]; - interceptDismissed: boolean; - prettifyJSON: boolean; - wrapLogMessage: boolean; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel = { + key: string; + value: string; }; -export type LogsDrilldownDefaultsOperatorState = { +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels = LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel[]; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord = { + columns: string[]; + labels: LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels; +}; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords = + LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord[]; +export type LogsDrilldownDefaultColumnsSpec = { + records: LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords; +}; +export type LogsDrilldownDefaultColumnsOperatorState = { /** 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 */ @@ -895,7 +1297,7 @@ export type LogsDrilldownDefaultsOperatorState = { It is limited to three possible states for machine evaluation. */ state: 'success' | 'in_progress' | 'failed'; }; -export type LogsDrilldownDefaultsStatus = { +export type LogsDrilldownDefaultColumnsStatus = { /** additionalFields is reserved for future use */ additionalFields?: { [key: string]: { @@ -905,17 +1307,17 @@ export type LogsDrilldownDefaultsStatus = { /** 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?: { - [key: string]: LogsDrilldownDefaultsOperatorState; + [key: string]: LogsDrilldownDefaultColumnsOperatorState; }; }; -export type LogsDrilldownDefaults = { +export type LogsDrilldownDefaultColumns = { /** 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: LogsDrilldownDefaultsSpec; - status?: LogsDrilldownDefaultsStatus; + spec: LogsDrilldownDefaultColumnsSpec; + status?: LogsDrilldownDefaultColumnsStatus; }; 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. */ @@ -927,10 +1329,10 @@ export type ListMeta = { /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ selfLink?: string; }; -export type LogsDrilldownDefaultsList = { +export type LogsDrilldownDefaultColumnsList = { /** 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: LogsDrilldownDefaults[]; + items: LogsDrilldownDefaultColumns[]; /** 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; @@ -980,6 +1382,57 @@ export type Status = { status?: string; }; export type Patch = object; +export type LogsDrilldownDefaultsSpec = { + defaultFields: string[]; + interceptDismissed: boolean; + prettifyJSON: boolean; + wrapLogMessage: boolean; +}; +export type LogsDrilldownDefaultsOperatorState = { + /** 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?: { + [key: string]: { + [key: string]: any; + }; + }; + /** 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'; +}; +export type LogsDrilldownDefaultsStatus = { + /** additionalFields is reserved for future use */ + additionalFields?: { + [key: string]: { + [key: string]: any; + }; + }; + /** 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?: { + [key: string]: LogsDrilldownDefaultsOperatorState; + }; +}; +export type LogsDrilldownDefaults = { + /** 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: LogsDrilldownDefaultsSpec; + status?: LogsDrilldownDefaultsStatus; +}; +export type LogsDrilldownDefaultsList = { + /** 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: LogsDrilldownDefaults[]; + /** 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 LogsDrilldownSpec = { defaultFields: string[]; interceptDismissed: boolean; @@ -1034,6 +1487,19 @@ export type LogsDrilldownList = { export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, + useListLogsDrilldownDefaultColumnsQuery, + useLazyListLogsDrilldownDefaultColumnsQuery, + useCreateLogsDrilldownDefaultColumnsMutation, + useDeletecollectionLogsDrilldownDefaultColumnsMutation, + useGetLogsDrilldownDefaultColumnsQuery, + useLazyGetLogsDrilldownDefaultColumnsQuery, + useReplaceLogsDrilldownDefaultColumnsMutation, + useDeleteLogsDrilldownDefaultColumnsMutation, + useUpdateLogsDrilldownDefaultColumnsMutation, + useGetLogsDrilldownDefaultColumnsStatusQuery, + useLazyGetLogsDrilldownDefaultColumnsStatusQuery, + useReplaceLogsDrilldownDefaultColumnsStatusMutation, + useUpdateLogsDrilldownDefaultColumnsStatusMutation, useListLogsDrilldownDefaultsQuery, useLazyListLogsDrilldownDefaultsQuery, useCreateLogsDrilldownDefaultsMutation, 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 7ef1fc4fc91..be22503f3bf 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,5 +1,5 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'Job', 'Repository', 'Provisioning'] as const; +export const addTagTypes = ['API Discovery', 'Connection', 'Job', 'Repository', 'Provisioning'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -10,6 +10,156 @@ const injectedRtkApi = api query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), + listConnection: build.query({ + query: (queryArg) => ({ + url: `/connections`, + 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: ['Connection'], + }), + createConnection: build.mutation({ + query: (queryArg) => ({ + url: `/connections`, + method: 'POST', + body: queryArg.connection, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Connection'], + }), + deletecollectionConnection: build.mutation< + DeletecollectionConnectionApiResponse, + DeletecollectionConnectionApiArg + >({ + query: (queryArg) => ({ + url: `/connections`, + 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: ['Connection'], + }), + getConnection: build.query({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Connection'], + }), + replaceConnection: build.mutation({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}`, + method: 'PUT', + body: queryArg.connection, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Connection'], + }), + deleteConnection: build.mutation({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Connection'], + }), + updateConnection: build.mutation({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Connection'], + }), + getConnectionStatus: build.query({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Connection'], + }), + replaceConnectionStatus: build.mutation({ + query: (queryArg) => ({ + url: `/connections/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.connection, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Connection'], + }), + updateConnectionStatus: build.mutation({ + query: (queryArg) => ({ + url: `/connections/${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: ['Connection'], + }), listJob: build.query({ query: (queryArg) => ({ url: `/jobs`, @@ -411,6 +561,208 @@ const injectedRtkApi = api export { injectedRtkApi as generatedAPI }; export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; export type GetApiResourcesApiArg = void; +export type ListConnectionApiResponse = /** status 200 OK */ ConnectionList; +export type ListConnectionApiArg = { + /** 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 CreateConnectionApiResponse = /** status 200 OK */ + | Connection + | /** status 201 Created */ Connection + | /** status 202 Accepted */ Connection; +export type CreateConnectionApiArg = { + /** 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; + connection: Connection; +}; +export type DeletecollectionConnectionApiResponse = /** status 200 OK */ Status; +export type DeletecollectionConnectionApiArg = { + /** 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 GetConnectionApiResponse = /** status 200 OK */ Connection; +export type GetConnectionApiArg = { + /** name of the Connection */ + 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 ReplaceConnectionApiResponse = /** status 200 OK */ Connection | /** status 201 Created */ Connection; +export type ReplaceConnectionApiArg = { + /** name of the Connection */ + 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; + connection: Connection; +}; +export type DeleteConnectionApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteConnectionApiArg = { + /** name of the Connection */ + 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 UpdateConnectionApiResponse = /** status 200 OK */ Connection | /** status 201 Created */ Connection; +export type UpdateConnectionApiArg = { + /** name of the Connection */ + 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 GetConnectionStatusApiResponse = /** status 200 OK */ Connection; +export type GetConnectionStatusApiArg = { + /** name of the Connection */ + 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 ReplaceConnectionStatusApiResponse = /** status 200 OK */ Connection | /** status 201 Created */ Connection; +export type ReplaceConnectionStatusApiArg = { + /** name of the Connection */ + 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; + connection: Connection; +}; +export type UpdateConnectionStatusApiResponse = /** status 200 OK */ Connection | /** status 201 Created */ Connection; +export type UpdateConnectionStatusApiArg = { + /** name of the Connection */ + 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 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). */ @@ -1053,6 +1405,169 @@ export type ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; +export type InlineSecureValue = + | { + /** Create a secure value -- this is only used for POST/PUT */ + create?: string; + /** Name in the secret service (reference) */ + name: string; + /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ + remove?: boolean; + } + | { + /** Create a secure value -- this is only used for POST/PUT */ + create: string; + /** Name in the secret service (reference) */ + name?: string; + /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ + remove?: boolean; + } + | { + /** Create a secure value -- this is only used for POST/PUT */ + create?: string; + /** Name in the secret service (reference) */ + name?: string; + /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ + remove: boolean; + }; +export type ConnectionSecure = { + /** ClientSecret is the reference to the secret used for other providers authentication, and Github on-behalf-of authentication. This value is stored securely and cannot be read back */ + clientSecret?: InlineSecureValue; + /** PrivateKey is the reference to the private key used for GitHub App authentication. This value is stored securely and cannot be read back */ + privateKey?: InlineSecureValue; + /** Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back */ + webhook?: InlineSecureValue; +}; +export type BitbucketConnectionConfig = { + /** App client ID */ + clientID: string; +}; +export type GitHubConnectionConfig = { + /** GitHub App ID */ + appID: string; + /** GitHub App installation ID */ + installationID: string; +}; +export type GitlabConnectionConfig = { + /** App client ID */ + clientID: string; +}; +export type ConnectionSpec = { + /** Bitbucket connection configuration Only applicable when provider is "bitbucket" */ + bitbucket?: BitbucketConnectionConfig; + /** GitHub connection configuration Only applicable when provider is "github" */ + github?: GitHubConnectionConfig; + /** Gitlab connection configuration Only applicable when provider is "gitlab" */ + gitlab?: GitlabConnectionConfig; + /** The connection provider type + + Possible enum values: + - `"bitbucket"` + - `"github"` + - `"gitlab"` */ + type: 'bitbucket' | 'github' | 'gitlab'; + /** The connection URL */ + url?: string; +}; +export type HealthStatus = { + /** When the health was checked last time */ + checked?: number; + /** The type of the error + + Possible enum values: + - `"health"` + - `"hook"` */ + error?: 'health' | 'hook'; + /** When not healthy, requests will not be executed */ + healthy: boolean; + /** Summary messages (can be shown to users) Will only be populated when not healthy */ + message?: string[]; +}; +export type ConnectionStatus = { + /** The connection health status */ + health: HealthStatus; + /** The generation of the spec last time reconciliation ran */ + observedGeneration: number; + /** Connection state + + Possible enum values: + - `"connected"` + - `"disconnected"` */ + state: 'connected' | 'disconnected'; +}; +export type Connection = { + /** 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; + secure?: ConnectionSecure; + spec?: ConnectionSpec; + status?: ConnectionStatus; +}; +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 ConnectionList = { + /** 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: Connection[]; + /** 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 ResourceRef = { /** Group is the group of the resource, such as "dashboard.grafana.app". */ group?: string; @@ -1070,8 +1585,6 @@ export type DeleteJobOptions = { resources?: ResourceRef[]; }; export type MigrateJobOptions = { - /** Preserve history (if possible) */ - history?: boolean; /** Message to use when committing the changes in a single commit */ message?: string; }; @@ -1138,7 +1651,7 @@ export type JobResourceSummary = { delete?: number; /** Create or update (export) */ error?: number; - /** Report errors for this resource type This may not be an exhaustive list and recommend looking at the logs for more info */ + /** Report errors/warnings for this resource type This may not be an exhaustive list and recommend looking at the logs for more info */ errors?: string[]; group?: string; kind?: string; @@ -1146,6 +1659,9 @@ export type JobResourceSummary = { noop?: number; total?: number; update?: number; + /** The error count */ + warning?: number; + warnings?: string[]; write?: number; }; export type RepositoryUrLs = { @@ -1176,6 +1692,7 @@ export type JobStatus = { summary?: JobResourceSummary[]; /** URLs contains URLs for the reference branch or commit if applicable. */ url?: RepositoryUrLs; + warnings?: string[]; }; export type Job = { /** 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 */ @@ -1186,16 +1703,6 @@ export type Job = { spec?: JobSpec; status?: JobStatus; }; -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 JobList = { /** 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; @@ -1204,76 +1711,6 @@ export type JobList = { 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 InlineSecureValue = - | { - /** Create a secure value -- this is only used for POST/PUT */ - create?: string; - /** Name in the secret service (reference) */ - name: string; - /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ - remove?: boolean; - } - | { - /** Create a secure value -- this is only used for POST/PUT */ - create: string; - /** Name in the secret service (reference) */ - name?: string; - /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ - remove?: boolean; - } - | { - /** Create a secure value -- this is only used for POST/PUT */ - create?: string; - /** Name in the secret service (reference) */ - name?: string; - /** Remove this value from the secure value map Values owned by this resource will be deleted if necessary */ - remove: boolean; - }; export type SecureValues = { /** Token used to connect the configured repository */ token?: InlineSecureValue; @@ -1292,6 +1729,9 @@ export type BitbucketRepositoryConfig = { /** The repository URL (e.g. `https://bitbucket.org/example/test`). */ url?: string; }; +export type ConnectionInfo = { + name: string; +}; export type GitRepositoryConfig = { /** The branch to use in the repository. */ branch: string; @@ -1344,6 +1784,8 @@ export type SyncOptions = { export type RepositorySpec = { /** The repository on Bitbucket. Mutually exclusive with local | github | git. */ bitbucket?: BitbucketRepositoryConfig; + /** The connection the repository references. This means the Repository is interacting with git via a Connection. */ + connection?: ConnectionInfo; /** Repository description */ description?: string; /** The repository on Git. Mutually exclusive with local | github | git. */ @@ -1370,20 +1812,6 @@ export type RepositorySpec = { /** UI driven Workflow that allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly) */ workflows: ('branch' | 'write')[]; }; -export type HealthStatus = { - /** When the health was checked last time */ - checked?: number; - /** The type of the error - - Possible enum values: - - `"health"` - - `"hook"` */ - error?: 'health' | 'hook'; - /** When not healthy, requests will not be executed */ - healthy: boolean; - /** Summary messages (can be shown to users) Will only be populated when not healthy */ - message?: string[]; -}; export type ResourceCount = { count: number; group: string; @@ -1617,8 +2045,6 @@ export type RepositoryViewList = { items: RepositoryView[]; /** 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; - /** The backend is using legacy storage FIXME: Not sure where this should be exposed... but we need it somewhere The UI should force the onboarding workflow when this is true */ - legacyStorage?: boolean; }; export type ManagerStats = { /** Manager identity */ @@ -1644,6 +2070,19 @@ export type ResourceStats = { export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, + useListConnectionQuery, + useLazyListConnectionQuery, + useCreateConnectionMutation, + useDeletecollectionConnectionMutation, + useGetConnectionQuery, + useLazyGetConnectionQuery, + useReplaceConnectionMutation, + useDeleteConnectionMutation, + useUpdateConnectionMutation, + useGetConnectionStatusQuery, + useLazyGetConnectionStatusQuery, + useReplaceConnectionStatusMutation, + useUpdateConnectionStatusMutation, useListJobQuery, useLazyListJobQuery, useCreateJobMutation, diff --git a/packages/grafana-api-clients/src/generator/helpers.ts b/packages/grafana-api-clients/src/generator/helpers.ts index 4563a042eb1..70960c34eea 100644 --- a/packages/grafana-api-clients/src/generator/helpers.ts +++ b/packages/grafana-api-clients/src/generator/helpers.ts @@ -143,8 +143,10 @@ export const updatePackageJsonExports = // Create the new export entry const newExportKey = `./rtkq/${groupName}/${version}`; const newExportValue = { - import: `./src/clients/rtkq/${groupName}/${version}/index.ts`, - require: `./src/clients/rtkq/${groupName}/${version}/index.ts`, + '@grafana-app/source': `./src/clients/rtkq/${groupName}/${version}/index.ts`, + types: `./dist/types/clients/rtkq/${groupName}/${version}/index.d.ts`, + import: `./dist/esm/clients/rtkq/${groupName}/${version}/index.mjs`, + require: `./dist/cjs/clients/rtkq/${groupName}/${version}/index.cjs`, }; // Check if export already exists diff --git a/packages/grafana-api-clients/tsconfig.json b/packages/grafana-api-clients/tsconfig.json index ed1c294703e..e75b58104b0 100644 --- a/packages/grafana-api-clients/tsconfig.json +++ b/packages/grafana-api-clients/tsconfig.json @@ -8,7 +8,8 @@ "emitDeclarationOnly": true, "isolatedModules": true, "rootDirs": ["."], - "allowImportingTsExtensions": true + "allowImportingTsExtensions": true, + "moduleResolution": "bundler" }, "exclude": ["dist/**/*"], "include": [ @@ -17,5 +18,12 @@ "../grafana-ui/src/types/*.d.ts", "../grafana-i18n/src/types/*.d.ts", "src/**/*.ts*" - ] + ], + "ts-node": { + "swc": true, + "compilerOptions": { + "module": "es2020", + "moduleResolution": "Bundler" + } + } } diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index ff90b2c295d..df595973cca 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -13,32 +13,31 @@ "url": "http://github.com/grafana/grafana.git", "directory": "packages/grafana-data" }, - "main": "src/index.ts", - "types": "src/index.ts", - "module": "src/index.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/types/index.d.ts", "exports": { "./package.json": "./package.json", ".": { - "import": "./src/index.ts", - "require": "./src/index.ts" - }, - "./internal": { - "import": "./src/internal/index.ts", - "require": "./src/internal/index.ts" + "@grafana-app/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" }, "./unstable": { - "import": "./src/unstable.ts", - "require": "./src/unstable.ts" + "@grafana-app/source": "./src/unstable.ts", + "types": "./dist/types/unstable.d.ts", + "import": "./dist/esm/unstable.mjs", + "require": "./dist/cjs/unstable.cjs" + }, + "./internal": { + "@grafana-app/source": "./src/internal/index.ts" }, "./test": { - "import": "./test/index.ts", - "require": "./test/index.ts" + "@grafana-app/source": "./test/index.ts" } }, "publishConfig": { - "main": "./dist/cjs/index.cjs", - "module": "./dist/esm/index.mjs", - "types": "./dist/types/index.d.ts", "access": "public" }, "files": [ @@ -51,8 +50,8 @@ "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./unstable ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", - "prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js", - "postpack": "mv package.json.bak package.json && rimraf ./unstable" + "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", + "postpack": "mv package.json.bak package.json" }, "dependencies": { "@braintree/sanitize-url": "7.0.1", diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 378ab225769..87008ddc45f 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -9,13 +9,13 @@ export default [ { input: entryPoint, plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')], + output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, { input: 'src/unstable.ts', plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')], + output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, ]; diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index 718c0e54430..5ec3ed7ba4f 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -3,11 +3,18 @@ import { merge } from 'lodash'; import { toDataFrame } from '../dataframe/processDataFrame'; import { createTheme } from '../themes/createTheme'; import { ReducerID } from '../transformations/fieldReducer'; +import { FieldType } from '../types/dataFrame'; import { FieldConfigPropertyItem } from '../types/fieldOverrides'; import { MappingType, SpecialValueMatch, ValueMapping } from '../types/valueMapping'; import { getDisplayProcessor } from './displayProcessor'; -import { fixCellTemplateExpressions, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay'; +import { + FieldSparkline, + fixCellTemplateExpressions, + getFieldDisplayValues, + GetFieldDisplayValuesOptions, + getSparklineHighlight, +} from './fieldDisplay'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; describe('FieldDisplay', () => { @@ -556,3 +563,71 @@ describe('fixCellTemplateExpressions', () => { ); }); }); + +describe('getSparklineHighlight', () => { + const sparkline: FieldSparkline = { + y: { name: 'A', type: FieldType.number, values: [null, 2, 3, 4, 10, 8, 8, 8, 9, null], config: {} }, + }; + + it.each([ + { + calc: ReducerID.last, + expected: { + type: 'point', + xIdx: 9, + }, + }, + { + calc: ReducerID.max, + expected: { + type: 'point', + xIdx: 4, + }, + }, + { + calc: ReducerID.min, + expected: { + type: 'point', + xIdx: 1, + }, + }, + { + calc: ReducerID.first, + expected: { + type: 'point', + xIdx: 0, + }, + }, + { + calc: ReducerID.firstNotNull, + expected: { + type: 'point', + xIdx: 1, + }, + }, + { + calc: ReducerID.lastNotNull, + expected: { + type: 'point', + xIdx: 8, + }, + }, + { + calc: ReducerID.mean, + expected: { + type: 'line', + y: 6.5, + }, + }, + { + calc: ReducerID.median, + expected: { + type: 'line', + y: 8, + }, + }, + ])('it calculates the correct highlight for the $calc', ({ calc, expected }) => { + const result = getSparklineHighlight(sparkline, calc); + expect(result).toEqual(expected); + }); +}); diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index c4423173da3..3496f419395 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -3,7 +3,7 @@ import { isEmpty } from 'lodash'; import { DataFrameView } from '../dataframe/DataFrameView'; import { getTimeField } from '../dataframe/processDataFrame'; import { GrafanaTheme2 } from '../themes/types'; -import { reduceField, ReducerID } from '../transformations/fieldReducer'; +import { isReducerID, reduceField, ReducerID } from '../transformations/fieldReducer'; import { getFieldMatcher } from '../transformations/matchers'; import { FieldMatcherID } from '../transformations/matchers/ids'; import { ScopedVars } from '../types/ScopedVars'; @@ -43,6 +43,7 @@ export interface FieldSparkline { x?: Field; // if this does not exist, use the index timeRange?: TimeRange; // Optionally force an absolute time highlightIndex?: number; + highlightLine?: number; } export interface FieldDisplay { @@ -72,6 +73,76 @@ export interface GetFieldDisplayValuesOptions { export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25; +interface SparklineHighlightPoint { + type: 'point'; + xIdx: number; +} + +interface SparklineHighlightLine { + type: 'line'; + y: number; +} + +export function getSparklineHighlight( + sparkline: FieldSparkline, + calc: ReducerID +): SparklineHighlightPoint | SparklineHighlightLine | void { + switch (calc) { + case ReducerID.last: + return { type: 'point', xIdx: sparkline.y.values.length - 1 }; + case ReducerID.first: + return { type: 'point', xIdx: 0 }; + case ReducerID.lastNotNull: { + for (let k = sparkline.y.values.length - 1; k >= 0; k--) { + const v = sparkline.y.values[k]; + if (v !== null && v !== undefined && !Number.isNaN(v)) { + return { type: 'point', xIdx: k }; + } + } + return; + } + case ReducerID.firstNotNull: { + for (let k = 0; k < sparkline.y.values.length; k++) { + const v = sparkline.y.values[k]; + if (v !== null && v !== undefined && !Number.isNaN(v)) { + return { type: 'point', xIdx: k }; + } + } + return; + } + case ReducerID.min: { + let minIdx = -1; + let prevMin = Infinity; + for (let k = 0; k < sparkline.y.values.length; k++) { + const v = sparkline.y.values[k]; + if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) { + prevMin = v; + minIdx = k; + } + } + return minIdx >= 0 ? { type: 'point', xIdx: minIdx } : undefined; + } + case ReducerID.max: { + let maxIdx = -1; + let prevMax = -Infinity; + for (let k = 0; k < sparkline.y.values.length; k++) { + const v = sparkline.y.values[k]; + if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) { + prevMax = v; + maxIdx = k; + } + } + return maxIdx >= 0 ? { type: 'point', xIdx: maxIdx } : undefined; + } + case ReducerID.mean: + return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.mean] }).mean }; + case ReducerID.median: + return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.median] }).median }; + default: + return; + } +} + export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => { const { replaceVariables, reduceOptions, timeZone, theme } = options; const calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last]; @@ -190,10 +261,16 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi y: dataFrame.fields[i], x: timeField, }; - if (calc === ReducerID.last) { - sparkline.highlightIndex = sparkline.y.values.length - 1; - } else if (calc === ReducerID.first) { - sparkline.highlightIndex = 0; + if (isReducerID(calc)) { + const sparklineHighlight = getSparklineHighlight(sparkline, calc); + switch (sparklineHighlight?.type) { + case 'point': + sparkline.highlightIndex = sparklineHighlight.xIdx; + break; + case 'line': + sparkline.highlightLine = sparklineHighlight.y; + break; + } } } diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 901fc6d8cc2..767da439543 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -253,7 +253,7 @@ describe('applyFieldOverrides', () => { ], }); - it('will apply field overrides to the fields within the frame', () => { + it('will apply default field overrides to the fields within the frame', () => { const f0 = createDataFrame({ name: 'A', fields: [ @@ -285,6 +285,41 @@ describe('applyFieldOverrides', () => { expect(withOverrides[0].fields[1].values[0].fields[1].state.range.max).toBe(30); }); + it('will apply targeted field overrides to the fields within the frame', () => { + const f0 = createDataFrame({ + name: 'A', + fields: [ + { + name: 'message', + type: FieldType.string, + values: ['foo'], + }, + { + name: 'frame', + type: FieldType.frame, + values: [f0Internal], + }, + ], + }); + const withOverrides = applyFieldOverrides({ + data: [f0], + fieldConfig: { + defaults: {}, + overrides: [ + { + matcher: { id: FieldMatcherID.byName, options: 'frame' }, + properties: [{ id: 'max', value: 30 }], + }, + ], + }, + replaceVariables: (value) => value, + theme: createTheme(), + fieldConfigRegistry: customFieldRegistry, + }); + + expect(withOverrides[0].fields[1].values[0].fields[1].config.max).toBe(30); + }); + it('will not crash when some of the nested frames are undefined', () => { const f0 = createDataFrame({ name: 'A', diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 2aac477cd2b..8b345036c64 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -1,4 +1,4 @@ -import { isNumber, set, unset, get, cloneDeep } from 'lodash'; +import { isNumber, set, unset, get, cloneDeep, defaultsDeep } from 'lodash'; import { createContext, useContext, useMemo, useRef } from 'react'; import { usePrevious } from 'react-use'; @@ -240,9 +240,14 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra ...options, // nested frames can be `undefined` in certain situations, like after `merge` transform due to padding the value array. // let's replace them with empty frames to avoid errors applying overrides - data: field.values.map( - (nestedFrame: DataFrame | undefined): DataFrame => nestedFrame ?? createDataFrame({ fields: [] }) - ), + data: field.values.map((nestedFrame: DataFrame | undefined): DataFrame => { + const result = nestedFrame ?? createDataFrame({ fields: [] }); + result.fields = result.fields.map((newField) => { + newField.config = defaultsDeep(newField.config || {}, config); + return newField; + }); + return result; + }), }); } } @@ -256,7 +261,7 @@ function calculateRange( field: Field, globalRange: NumericRange | undefined, data: DataFrame[] -): { range?: { min?: number | null; max?: number | null; delta: number }; newGlobalRange: NumericRange | undefined } { +): { range?: NumericRange; newGlobalRange?: NumericRange } { // If range is defined with min/max, use it if (isNumber(config.min) && isNumber(config.max)) { const range = { min: config.min, max: config.max, delta: config.max - config.min }; diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 5084c577176..6027b566764 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -521,6 +521,7 @@ export { VariableRefresh, VariableSort, VariableHide, + type VariableRegexApplyTo, type VariableType, type VariableModel, type TypedVariableModel, @@ -663,6 +664,7 @@ export { type DataSourceGetTagKeysOptions, type DataSourceGetTagValuesOptions, type DataSourceGetDrilldownsApplicabilityOptions, + type DataSourceGetRecommendedDrilldownsOptions, type MetadataInspectorProps, type LegacyMetricFindQueryOptions, type QueryEditorProps, @@ -680,6 +682,7 @@ export { type QueryHint, type MetricFindValue, type DrilldownsApplicability, + type DrilldownRecommendation, type DataSourceJsonData, type DataSourceSettings, type DataSourceInstanceSettings, diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index f519b1d56b4..b2d3c16a3b1 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -289,6 +289,7 @@ export interface GrafanaConfig { rudderstackWriteKey: string; rudderstackDataPlaneUrl: string; rudderstackSdkUrl: string; + rudderstackV3SdkUrl: string; rudderstackConfigUrl: string; rudderstackIntegrationsUrl: string; applicationInsightsConnectionString: string; diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 538e7a051e8..f50e126a5c7 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -313,6 +313,13 @@ abstract class DataSourceApi< options?: DataSourceGetDrilldownsApplicabilityOptions ): Promise; + /** + * Get recommended drilldowns for a dashboard + */ + getRecommendedDrilldowns?( + options?: DataSourceGetRecommendedDrilldownsOptions + ): Promise; + /** * Get tag keys for adhoc filters */ @@ -398,13 +405,9 @@ abstract class DataSourceApi< } /** - * Options argument to DataSourceAPI.getTagKeys + * Base options shared across datasource filtering operations. */ -export interface DataSourceGetTagKeysOptions { - /** - * The other existing filters or base filters. New in v10.3 - */ - filters: AdHocVariableFilter[]; +export interface DataSourceFilteringRequestOptions { /** * Context time range. New in v10.3 */ @@ -413,21 +416,27 @@ export interface DataSourceGetTagKeysOptions + extends DataSourceFilteringRequestOptions { + /** + * The other existing filters or base filters. New in v10.3 + */ + filters: AdHocVariableFilter[]; +} + /** * Options argument to DataSourceAPI.getTagValues */ -export interface DataSourceGetTagValuesOptions { +export interface DataSourceGetTagValuesOptions + extends DataSourceFilteringRequestOptions { key: string; /** * The other existing filters or base filters. New in v10.3 */ filters: AdHocVariableFilter[]; - /** - * Context time range. New in v10.3 - */ - timeRange?: TimeRange; - queries?: TQuery[]; - scopes?: Scope[] | undefined; } export interface MetadataInspectorProps< @@ -646,12 +655,22 @@ export interface MetricFindValue { properties?: Record; } -export interface DataSourceGetDrilldownsApplicabilityOptions { +export interface DataSourceGetDrilldownsApplicabilityOptions + extends DataSourceFilteringRequestOptions { + filters?: AdHocVariableFilter[]; + groupByKeys?: string[]; +} + +export interface DataSourceGetRecommendedDrilldownsOptions + extends DataSourceFilteringRequestOptions { + dashboardUid?: string; + filters?: AdHocVariableFilter[]; + groupByKeys?: string[]; +} + +export interface DrilldownRecommendation { filters?: AdHocVariableFilter[]; groupByKeys?: string[]; - timeRange?: TimeRange; - queries?: TQuery[]; - scopes?: Scope[] | undefined; } export interface DrilldownsApplicability { diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index eba76d2c198..aebbab8c6f9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -126,11 +126,6 @@ export interface FeatureToggles { */ disableSSEDataplane?: boolean; /** - * Writes error logs to the request logger - * @default true - */ - unifiedRequestLog?: boolean; - /** * Uses JWT-based auth for rendering instead of relying on remote cache */ renderAuthJWT?: boolean; @@ -261,6 +256,10 @@ export interface FeatureToggles { */ kubernetesCorrelations?: boolean; /** + * Adds support for Kubernetes unified storage quotas + */ + kubernetesUnifiedStorageQuotas?: boolean; + /** * Adds support for Kubernetes logs drilldown */ kubernetesLogsDrilldown?: boolean; @@ -305,10 +304,6 @@ export interface FeatureToggles { */ queryServiceFromUI?: boolean; /** - * Routes explore requests to the new query service - */ - queryServiceFromExplore?: boolean; - /** * Runs CloudWatch metrics queries as separate batches */ cloudWatchBatchQueries?: boolean; @@ -361,10 +356,6 @@ export interface FeatureToggles { */ dashboardNewLayouts?: boolean; /** - * Use the v2 kubernetes API in the frontend for dashboards - */ - kubernetesDashboardsV2?: boolean; - /** * Enables undo/redo in dynamic dashboards */ dashboardUndoRedo?: boolean; @@ -373,6 +364,10 @@ export interface FeatureToggles { */ unlimitedLayoutsNesting?: boolean; /** + * Enables showing recently used drilldowns or recommendations given by the datasource in the AdHocFilters and GroupBy variables + */ + drilldownRecommendations?: boolean; + /** * Enables viewing non-applicable drilldowns on a panel level */ perPanelNonApplicableDrilldowns?: boolean; @@ -422,6 +417,10 @@ export interface FeatureToggles { */ jitterAlertRulesWithinGroups?: boolean; /** + * Enable audit logging with Kubernetes under app platform + */ + auditLoggingAppPlatform?: boolean; + /** * Enable the secrets management API and services under app platform */ secretsManagementAppPlatform?: boolean; @@ -495,6 +494,10 @@ export interface FeatureToggles { */ newDashboardWithFiltersAndGroupBy?: boolean; /** + * Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it + */ + dashboardAdHocAndGroupByWrapper?: boolean; + /** * Updates CloudWatch label parsing to be more accurate * @default true */ @@ -532,6 +535,10 @@ export interface FeatureToggles { */ alertingListViewV2?: boolean; /** + * Enables saved searches for alert rules list + */ + alertingSavedSearches?: boolean; + /** * Disables the ability to send alerts to an external Alertmanager datasource. */ alertingDisableSendAlertsExternal?: boolean; @@ -824,6 +831,10 @@ export interface FeatureToggles { */ fetchRulesUsingPost?: boolean; /** + * Add compact=true when fetching rules + */ + fetchRulesInCompactMode?: boolean; + /** * Enables the new logs panel * @default true */ @@ -984,6 +995,11 @@ export interface FeatureToggles { */ restoreDashboards?: boolean; /** + * Enables recently viewed dashboards section in the browsing dashboard page + * @default false + */ + recentlyViewedDashboards?: boolean; + /** * Enable configuration of alert enrichments in Grafana Cloud. * @default false */ @@ -1152,6 +1168,11 @@ export interface FeatureToggles { */ externalVizSuggestions?: boolean; /** + * Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows) + * @default false + */ + heatmapRowsAxisOptions?: boolean; + /** * Restrict PanelChrome contents with overflow: hidden; * @default true */ @@ -1176,10 +1197,20 @@ export interface FeatureToggles { */ onlyStoreActionSets?: boolean; /** + * Show insights for plugins in the plugin details page + * @default false + */ + pluginInsights?: boolean; + /** * Enables a new panel time settings drawer */ panelTimeSettings?: boolean; /** + * Enables the raw DSL query editor in the Elasticsearch data source + * @default false + */ + elasticsearchRawDSLQuery?: boolean; + /** * Enables app platform API for annotations * @default false */ @@ -1220,4 +1251,8 @@ export interface FeatureToggles { * @default false */ useMTPlugins?: boolean; + /** + * Enables support for variables whose values can have multiple properties + */ + multiPropsVariables?: boolean; } diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index b49be0d5363..34e672b66f5 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -288,6 +288,7 @@ export const availableIconsIndex = { bitbucket: true, git: true, 'tachometer-fast': true, + 'tachometer-empty': true, 'cmab-logo': true, }; diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 94f1d97518c..b5e66d705f4 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -273,7 +273,7 @@ export interface DataSourceWithSupplementaryQueriesSupport): SupplementaryQueryType[]; /** * Returns a supplementary query to be used to fetch supplementary data based on the provided type and original query. * If the provided query is not suitable for the provided supplementary query type, undefined should be returned. @@ -283,7 +283,8 @@ export interface DataSourceWithSupplementaryQueriesSupport( datasource: DataSourceApi | (DataSourceApi & DataSourceWithSupplementaryQueriesSupport), - type: SupplementaryQueryType + type: SupplementaryQueryType, + dsRequest?: DataQueryRequest ): datasource is DataSourceApi & DataSourceWithSupplementaryQueriesSupport => { if (!datasource) { return false; @@ -293,7 +294,7 @@ export const hasSupplementaryQuerySupport = ( ('getDataProvider' in datasource || 'getSupplementaryRequest' in datasource) && 'getSupplementaryQuery' in datasource && 'getSupportedSupplementaryQueryTypes' in datasource && - datasource.getSupportedSupplementaryQueryTypes().includes(type) + datasource.getSupportedSupplementaryQueryTypes(dsRequest).includes(type) ); }; diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index e6feea4dd3f..8b6ed69463b 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -32,6 +32,8 @@ export enum VariableRefresh { onTimeRangeChanged, } +export type VariableRegexApplyTo = 'value' | 'text'; + export enum VariableSort { disabled, alphabeticalAsc, @@ -117,6 +119,7 @@ export interface QueryVariableModel extends VariableWithMultiSupport { queryValue?: string; query: any; regex: string; + regexApplyTo?: VariableRegexApplyTo; refresh: VariableRefresh; staticOptions?: VariableOption[]; staticOptionsOrder?: 'before' | 'after' | 'sorted'; diff --git a/packages/grafana-data/tsconfig.json b/packages/grafana-data/tsconfig.json index d13025d3115..8e6013e32d9 100644 --- a/packages/grafana-data/tsconfig.json +++ b/packages/grafana-data/tsconfig.json @@ -3,10 +3,12 @@ "compilerOptions": { "declaration": true, "jsx": "react-jsx", + "baseUrl": "./", "declarationDir": "./dist/types", "emitDeclarationOnly": true, "isolatedModules": true, - "rootDirs": ["."] + "rootDirs": ["."], + "moduleResolution": "bundler" }, "exclude": ["dist/**/*"], "include": [ diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 0dd5c5a9925..5527f4c0ca6 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -16,12 +16,19 @@ "url": "http://github.com/grafana/grafana.git", "directory": "packages/grafana-e2e-selectors" }, - "main": "src/index.ts", - "types": "src/index.ts", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "types": "./dist/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "@grafana-app/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, "publishConfig": { - "main": "./dist/cjs/index.cjs", - "module": "./dist/esm/index.mjs", - "types": "./dist/types/index.d.ts", "access": "public" }, "files": [ diff --git a/packages/grafana-e2e-selectors/rollup.config.ts b/packages/grafana-e2e-selectors/rollup.config.ts index 1d47540a42a..a17799cc9a7 100644 --- a/packages/grafana-e2e-selectors/rollup.config.ts +++ b/packages/grafana-e2e-selectors/rollup.config.ts @@ -9,7 +9,7 @@ export default [ { input: entryPoint, plugins, - output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-e2e-selectors')], + output: [cjsOutput(pkg, 'grafana-e2e-selectors'), esmOutput(pkg, 'grafana-e2e-selectors')], treeshake: false, }, ]; diff --git a/packages/grafana-e2e-selectors/src/index.ts b/packages/grafana-e2e-selectors/src/index.ts index 32b22b5bb52..5b078f1233f 100644 --- a/packages/grafana-e2e-selectors/src/index.ts +++ b/packages/grafana-e2e-selectors/src/index.ts @@ -1,5 +1,5 @@ /** - * A library containing the different design components of the Grafana ecosystem. + * A library containing e2e selectors for the Grafana ecosystem. * * @packageDocumentation */ diff --git a/packages/grafana-e2e-selectors/src/resolver.test.ts b/packages/grafana-e2e-selectors/src/resolver.test.ts index a306f9261ed..888cf4fdc54 100644 --- a/packages/grafana-e2e-selectors/src/resolver.test.ts +++ b/packages/grafana-e2e-selectors/src/resolver.test.ts @@ -51,7 +51,7 @@ describe('Resolver', () => { expect(pages.Alerting.AddAlertRule.url).toBe('/alerting/new'); }); - it('should throw error if an invalid semver range is used in versioned selector', () => { + it('should throw an error if an invalid semver range is used in versioned selector', () => { expect(() => resolveSelectors({ Alerting: { diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index aca18844459..7dd819a1512 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -3,7 +3,7 @@ // (a ); diff --git a/packages/grafana-ui/src/components/InteractiveTable/utils.ts b/packages/grafana-ui/src/components/InteractiveTable/utils.ts index 68017bbfd19..2b664b16f6d 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/utils.ts +++ b/packages/grafana-ui/src/components/InteractiveTable/utils.ts @@ -1,6 +1,6 @@ import { Column as RTColumn } from 'react-table'; -import { ExpanderCell, ExpanderHeader } from './Expander'; +import { EmptyExpanderHeader, ExpanderCell, ExpanderHeader } from './Expander'; import { Column } from './types'; export const EXPANDER_CELL_ID = '__expander' as const; @@ -18,9 +18,7 @@ export function getColumns( { id: EXPANDER_CELL_ID, Cell: ExpanderCell, - ...(showExpandAll && { - Header: ExpanderHeader, - }), + Header: showExpandAll ? ExpanderHeader : EmptyExpanderHeader, disableSortBy: true, width: 0, }, diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx index ffba1eb88de..2960de1f502 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx @@ -28,7 +28,7 @@ interface PropsWithMinColumnWidth extends GridPropsBase { /** For a responsive layout, fit as many columns while maintaining this minimum column width. * The real width will be calculated based on the theme spacing tokens: `theme.spacing(minColumnWidth)` */ - minColumnWidth?: ResponsiveProp<1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 44 | 55 | 72 | 89 | 144>; + minColumnWidth?: ResponsiveProp<1 | 2 | 3 | 5 | 8 | 13 | 16 | 21 | 34 | 44 | 55 | 72 | 89 | 144>; } /** 'columns' and 'minColumnWidth' are mutually exclusive */ diff --git a/packages/grafana-ui/src/components/Modal/getModalStyles.ts b/packages/grafana-ui/src/components/Modal/getModalStyles.ts index 3c124d24e68..f1e92cfa3f6 100644 --- a/packages/grafana-ui/src/components/Modal/getModalStyles.ts +++ b/packages/grafana-ui/src/components/Modal/getModalStyles.ts @@ -79,7 +79,7 @@ export const getModalStyles = (theme: GrafanaTheme2) => { modalContent: css({ overflow: 'auto', padding: theme.spacing(3, 3, 0, 3), - marginBottom: theme.spacing(3), + marginBottom: theme.spacing(2.5), scrollbarWidth: 'thin', width: '100%', diff --git a/packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx b/packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx index eb5c54d7b3b..0660b9a0118 100644 --- a/packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx @@ -16,17 +16,22 @@ interface Props { title?: string; offset?: number; dragClass?: string; + onDragStart?: (event: React.PointerEvent) => void; onOpenMenu?: () => void; } -export function HoverWidget({ menu, title, dragClass, children, offset = -32, onOpenMenu }: Props) { +export function HoverWidget({ menu, title, dragClass, children, offset = -32, onOpenMenu, onDragStart }: Props) { const styles = useStyles2(getStyles); const draggableRef = useRef(null); const selectors = e2eSelectors.components.Panels.Panel.HoverWidget; // Capture the pointer to keep the widget visible while dragging - const onPointerDown = useCallback((e: React.PointerEvent) => { - draggableRef.current?.setPointerCapture(e.pointerId); - }, []); + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + draggableRef.current?.setPointerCapture(e.pointerId); + onDragStart?.(e); + }, + [onDragStart] + ); const onPointerUp = useCallback((e: React.PointerEvent) => { draggableRef.current?.releasePointerCapture(e.pointerId); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 28a395c7d98..f969bbcf3f0 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -248,15 +248,17 @@ export function PanelChrome({ const onContentPointerDown = React.useCallback( (evt: React.PointerEvent) => { - // Ignore clicks inside buttons, links, canvas and svg elments + // When selected, ignore clicks inside buttons, links, canvas and svg elments // This does prevent a clicks inside a graphs from selecting panel as there is normal div above the canvas element that intercepts the click - if (evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) { + if (isSelected && evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) { + // Stop propagation otherwise row config editor will get selected + evt.stopPropagation(); return; } onSelect?.(evt); }, - [onSelect] + [isSelected, onSelect] ); const headerContent = ( @@ -384,6 +386,7 @@ export function PanelChrome({ menu={menu} title={typeof title === 'string' ? title : undefined} dragClass={dragClass} + onDragStart={onDragStart} offset={hoverHeaderOffset} onOpenMenu={onOpenMenu} > diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx index f54f3cd6f22..f59614acd53 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialArcPath.tsx @@ -1,52 +1,149 @@ -import { GaugeDimensions, toRad } from './utils'; +import { useId, memo, HTMLAttributes, ReactNode } from 'react'; -export interface RadialArcPathProps { - startAngle: number; - dimensions: GaugeDimensions; - color: string; - glowFilter?: string; +import { FieldDisplay } from '@grafana/data'; + +import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; +import { drawRadialArcPath, toRad } from './utils'; + +export interface RadialArcPathPropsBase { arcLengthDeg: number; + barEndcaps?: boolean; + dimensions: RadialGaugeDimensions; + fieldDisplay: FieldDisplay; roundedBars?: boolean; + shape: RadialShape; + endpointMarker?: 'point' | 'glow'; + startAngle: number; + glowFilter?: string; + endpointMarkerGlowFilter?: string; } -export function RadialArcPath({ - startAngle: angle, - dimensions, - color, - glowFilter, - arcLengthDeg, - roundedBars, -}: RadialArcPathProps) { - const { radius, centerX, centerY, barWidth } = dimensions; +interface RadialArcPathPropsWithColor extends RadialArcPathPropsBase { + color: string; +} - if (arcLengthDeg === 360) { - // For some reason a 100% full arc cannot be rendered - arcLengthDeg = 359.99; +interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase { + gradient: GradientStop[]; +} + +type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient; + +const ENDPOINT_MARKER_MIN_ANGLE = 10; +const DOT_OPACITY = 0.5; +const DOT_RADIUS_FACTOR = 0.4; +const MAX_DOT_RADIUS = 8; + +export const RadialArcPath = memo( + ({ + arcLengthDeg, + dimensions, + fieldDisplay, + roundedBars, + shape, + endpointMarker, + barEndcaps, + startAngle: angle, + glowFilter, + endpointMarkerGlowFilter, + ...rest + }: RadialArcPathProps) => { + const id = useId(); + + const bgDivStyle: HTMLAttributes['style'] = { width: '100%', height: '100%' }; + if ('color' in rest) { + bgDivStyle.backgroundColor = rest.color; + } else { + bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape); + } + + const { radius, centerX, centerY, barWidth } = dimensions; + + const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars); + + const startRadians = toRad(angle); + const endRadians = toRad(angle + arcLengthDeg); + + const xStart = centerX + radius * Math.cos(startRadians); + const yStart = centerY + radius * Math.sin(startRadians); + const xEnd = centerX + radius * Math.cos(endRadians); + const yEnd = centerY + radius * Math.sin(endRadians); + + const dotRadius = + endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2; + + let barEndcapColors: [string, string] | undefined; + let endpointMarks: ReactNode = null; + if ('gradient' in rest) { + if (endpointMarker && (rest.gradient?.length ?? 0) > 0) { + switch (endpointMarker) { + case 'point': + const [pointColorStart, pointColorEnd] = getEndpointMarkerColors( + rest.gradient!, + fieldDisplay.display.percent + ); + endpointMarks = ( + <> + {arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && ( + + )} + + + ); + break; + case 'glow': + const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE); + const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle); + const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle); + endpointMarks = + arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? ( + + ) : null; + break; + default: + break; + } + } + + if (barEndcaps) { + barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent); + } + } + + return ( + <> + {/* FIXME: optimize this by only using clippath + foreign obj for gradients */} + + + + + + +
+ + {barEndcapColors?.[0] && } + {barEndcapColors?.[1] && ( + + )} + + + {endpointMarks} + + ); } +); - const startRadians = toRad(angle); - const endRadians = toRad(angle + arcLengthDeg); - - let x1 = centerX + radius * Math.cos(startRadians); - let y1 = centerY + radius * Math.sin(startRadians); - let x2 = centerX + radius * Math.cos(endRadians); - let y2 = centerY + radius * Math.sin(endRadians); - - const largeArc = arcLengthDeg > 180 ? 1 : 0; - - const path = ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' '); - - return ( - - ); -} +RadialArcPath.displayName = 'RadialArcPath'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx index 2bacc3af3e8..719ec52c625 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBar.tsx @@ -1,97 +1,64 @@ -import { GrafanaTheme2 } from '@grafana/data'; +import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions, toRad } from './utils'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; export interface RadialBarProps { - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; - angleRange: number; angle: number; - startAngle: number; + angleRange: number; + dimensions: RadialGaugeDimensions; + fieldDisplay: FieldDisplay; + gradient?: GradientStop[]; roundedBars?: boolean; - spotlightStroke: string; + endpointMarker?: 'point' | 'glow'; + shape: RadialShape; + startAngle: number; glowFilter?: string; + endpointMarkerGlowFilter?: string; } export function RadialBar({ - dimensions, - colorDefs, - angleRange, angle, - startAngle, + angleRange, + dimensions, + fieldDisplay, + gradient, roundedBars, - spotlightStroke, + endpointMarker, + shape, + startAngle, glowFilter, + endpointMarkerGlowFilter, }: RadialBarProps) { const theme = useTheme2(); - + const colorProps = gradient ? { gradient } : { color: fieldDisplay.display.color ?? FALLBACK_COLOR }; return ( <> - - {/** Track */} - - {/** The colored bar */} - - {spotlightStroke && angle > 8 && ( - - )} - - {colorDefs.getDefs()} + {/** Track */} + + {/** The colored bar */} + ); } - -interface SpotlightEffectProps { - dimensions: GaugeDimensions; - angle: number; - glowFilter?: string; - spotlightStroke: string; - theme: GrafanaTheme2; - roundedBars?: boolean; -} - -function SpotlightSquareEffect({ dimensions, angle, glowFilter, spotlightStroke, roundedBars }: SpotlightEffectProps) { - const { radius, centerX, centerY, barWidth } = dimensions; - - const angleRadian = toRad(angle); - const x1 = centerX + radius * Math.cos(angleRadian - 0.2); - const y1 = centerY + radius * Math.sin(angleRadian - 0.2); - const x2 = centerX + radius * Math.cos(angleRadian); - const y2 = centerY + radius * Math.sin(angleRadian); - - const path = ['M', x1, y1, 'A', radius, radius, 0, 0, 1, x2, y2].join(' '); - - return ( - - ); -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx index dbc93d2da7f..b51cb4ce2f1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialBarSegmented.tsx @@ -1,126 +1,74 @@ -import { FieldDisplay } from '@grafana/data'; +import { memo } from 'react'; + +import { FALLBACK_COLOR, FieldDisplay } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialGaugeDimensions, GradientStop } from './types'; +import { + getAngleBetweenSegments, + getFieldConfigMinMax, + getFieldDisplayProcessor, + getOptimalSegmentCount, +} from './utils'; export interface RadialBarSegmentedProps { fieldDisplay: FieldDisplay; - dimensions: GaugeDimensions; - colorDefs: RadialColorDefs; + dimensions: RadialGaugeDimensions; angleRange: number; startAngle: number; glowFilter?: string; segmentCount: number; segmentSpacing: number; + shape: RadialShape; + gradient?: GradientStop[]; } -export function RadialBarSegmented({ - fieldDisplay, - dimensions, - startAngle, - angleRange, - glowFilter, - segmentCount, - segmentSpacing, - colorDefs, -}: RadialBarSegmentedProps) { - const segments: React.ReactNode[] = []; - const theme = useTheme2(); - const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange); - const min = fieldDisplay.field.min ?? 0; - const max = fieldDisplay.field.max ?? 100; - const value = fieldDisplay.display.numeric; - const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, angleRange); - const segmentArcLengthDeg = angleRange / segmentCountAdjusted - angleBetweenSegments; +export const RadialBarSegmented = memo( + ({ + fieldDisplay, + dimensions, + startAngle, + angleRange, + glowFilter, + gradient, + segmentCount, + segmentSpacing, + shape, + }: RadialBarSegmentedProps) => { + const theme = useTheme2(); + const segments: React.ReactNode[] = []; + const segmentCountAdjusted = getOptimalSegmentCount(dimensions, segmentSpacing, segmentCount, angleRange); + const [min, max] = getFieldConfigMinMax(fieldDisplay); + const value = fieldDisplay.display.numeric; + const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, angleRange); + const segmentArcLengthDeg = angleRange / segmentCountAdjusted - angleBetweenSegments; + const displayProcessor = getFieldDisplayProcessor(fieldDisplay); - for (let i = 0; i < segmentCountAdjusted; i++) { - const angleValue = min + ((max - min) / segmentCountAdjusted) * i; - const angleColor = colorDefs.getSegmentColor(angleValue); - const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; - const segmentColor = angleValue >= value ? theme.colors.action.hover : angleColor; + for (let i = 0; i < segmentCountAdjusted; i++) { + const angleValue = min + ((max - min) / segmentCountAdjusted) * i; + const segmentAngle = startAngle + (angleRange / segmentCountAdjusted) * i + 0.01; + const segmentColor = + angleValue >= value ? theme.colors.border.medium : (displayProcessor(angleValue).color ?? FALLBACK_COLOR); + const colorProps = angleValue < value && gradient ? { gradient } : { color: segmentColor }; - segments.push( - - ); + segments.push( + + ); + } + + return {segments}; } +); - return ( - <> - {segments} - {colorDefs.getDefs()} - - ); -} - -export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) { - // Max spacing is 8 degrees between segments - // Changing this constant could be considered a breaking change - const maxAngleBetweenSegments = Math.max(range / 1.5 / segmentCount, 2); - return segmentSpacing * maxAngleBetweenSegments; -} - -function getOptimalSegmentCount( - dimensions: GaugeDimensions, - segmentSpacing: number, - segmentCount: number, - range: number -) { - const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, range); - - const innerRadius = dimensions.radius - dimensions.barWidth / 2; - const circumference = Math.PI * innerRadius * 2 * (range / 360); - const maxSegments = Math.floor(circumference / (angleBetweenSegments + 3)); - - return Math.min(maxSegments, segmentCount); -} - -// export function RadialSegmentLine({ -// gaugeId, -// center, -// angle, -// size, -// color, -// barWidth, -// roundedBars, -// glow, -// margin, -// segmentWidth, -// }: RadialSegmentProps) { -// const arcSize = size - barWidth; -// const radius = arcSize / 2 - margin; - -// const angleRad = (Math.PI * (angle - 90)) / 180; -// const lineLength = radius - barWidth; - -// const x1 = center + radius * Math.cos(angleRad); -// const y1 = center + radius * Math.sin(angleRad); -// const x2 = center + lineLength * Math.cos(angleRad); -// const y2 = center + lineLength * Math.sin(angleRad); - -// return ( -// -// ); -// } +RadialBarSegmented.displayName = 'RadialBarSegmented'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx deleted file mode 100644 index 6bcf4876880..00000000000 --- a/packages/grafana-ui/src/components/RadialGauge/RadialColorDefs.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import tinycolor from 'tinycolor2'; - -import { DisplayProcessor, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; - -import { RadialGradientMode, RadialShape } from './RadialGauge'; -import { GaugeDimensions } from './utils'; - -export interface RadialColorDefsOptions { - gradient: RadialGradientMode; - fieldDisplay: FieldDisplay; - theme: GrafanaTheme2; - dimensions: GaugeDimensions; - shape: RadialShape; - gaugeId: string; - displayProcessor: DisplayProcessor; -} - -export class RadialColorDefs { - private colorToIds: Record = {}; - private defs: React.ReactNode[] = []; - - constructor(private options: RadialColorDefsOptions) {} - - getSegmentColor(forValue: number): string { - const { displayProcessor } = this.options; - const baseColor = displayProcessor(forValue).color ?? FALLBACK_COLOR; - - return this.getColor(baseColor, true); - } - - getColor(baseColor: string, forSegment?: boolean): string { - const { gradient, dimensions, gaugeId, fieldDisplay, shape, theme } = this.options; - - const id = `value-color-${baseColor}-${gaugeId}`; - - if (this.colorToIds[id]) { - return this.colorToIds[id]; - } - - // If no gradient, just return the base color - if (gradient === 'none') { - this.colorToIds[id] = baseColor; - return baseColor; - } - - const returnColor = (this.colorToIds[id] = `url(#${id})`); - const colorModeId = fieldDisplay.field.color?.mode; - const colorMode = getFieldColorMode(colorModeId); - const valuePercent = fieldDisplay.display.percent ?? 0; - - // Handle continusous color modes first - // If it's a segment color we don't want to do continuous gradients - if (colorMode.isContinuous && colorMode.getColors && !forSegment) { - const colors = colorMode.getColors(theme); - const count = colors.length; - - this.defs.push( - - {colors.map((stopColor, i) => ( - - ))} - - ); - - return returnColor; - } - - // For value based colors we want to stay more true to the specific color - // So a radial gradient that adds a bit of light and shade works best - if (colorMode.isByValue) { - const color1 = tinycolor(baseColor).darken(5); - - this.defs.push( - - - - - - ); - - return returnColor; - } - - // For fixed / palette based color scales we can create a more fun - // hue and light based linear gradient that we rotate/move with the value - - const x2 = shape === 'circle' ? 0 : dimensions.centerX + dimensions.radius; - const y2 = shape === 'circle' ? dimensions.centerY + dimensions.radius : 0; - const color1 = tinycolor(baseColor).spin(-20).darken(5); - const color2 = tinycolor(baseColor).saturate(20).spin(20).brighten(10); - - // this makes it so the gradient is always brightest at the current value - const transform = - shape === 'circle' - ? `rotate(${360 * valuePercent - 180} ${dimensions.centerX} ${dimensions.centerY})` - : `translate(-${dimensions.radius * 2 * (1 - valuePercent)}, 0)`; - - this.defs.push( - - {theme.isDark ? ( - <> - - - - ) : ( - <> - - - - )} - - ); - - return returnColor; - } - - getMainBarColor(): string { - return this.getColor(this.options.fieldDisplay.display.color ?? FALLBACK_COLOR); - } - - getDefs(): React.ReactNode[] { - return this.defs; - } -} diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx index c098beda886..5c5faa64db7 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.story.tsx @@ -13,7 +13,8 @@ import { FieldColorModeId } from '@grafana/schema'; import { useTheme2 } from '../../themes/ThemeContext'; import { Stack } from '../Layout/Stack/Stack'; -import { RadialGauge, RadialGaugeProps, RadialGradientMode, RadialShape, RadialTextMode } from './RadialGauge'; +import { RadialGauge, RadialGaugeProps } from './RadialGauge'; +import { RadialShape, RadialTextMode } from './types'; interface StoryProps extends RadialGaugeProps { value: number; @@ -34,7 +35,6 @@ const meta: Meta = { }, args: { barWidthFactor: 0.2, - spotlight: false, glowBar: false, glowCenter: false, sparkline: false, @@ -42,7 +42,7 @@ const meta: Meta = { width: 200, height: 200, shape: 'circle', - gradient: 'none', + gradient: false, seriesCount: 1, segmentCount: 0, segmentSpacing: 0.2, @@ -56,14 +56,14 @@ const meta: Meta = { width: { control: { type: 'range', min: 50, max: 600 } }, height: { control: { type: 'range', min: 50, max: 600 } }, value: { control: { type: 'range', min: 0, max: 110 } }, - spotlight: { control: 'boolean' }, roundedBars: { control: 'boolean' }, sparkline: { control: 'boolean' }, thresholdsBar: { control: 'boolean' }, - gradient: { control: { type: 'radio' } }, + gradient: { control: { type: 'boolean' } }, seriesCount: { control: { type: 'range', min: 1, max: 20 } }, segmentCount: { control: { type: 'range', min: 0, max: 100 } }, segmentSpacing: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }, + endpointMarker: { control: { type: 'select' }, options: ['none', 'point', 'glow'] }, colorScheme: { control: { type: 'select' }, options: [ @@ -102,57 +102,17 @@ export const Examples: StoryFn = (args) => {
Bar width
- - - - + + + +
Effects
- - - - + + + +
Shape: Gauge & color scale
@@ -160,14 +120,14 @@ export const Examples: StoryFn = (args) => { value={40} shape="gauge" width={250} - gradient="auto" + gradient colorScheme={FieldColorModeId.ContinuousGrYlRd} glowCenter={true} barWidthFactor={0.6} /> = (args) => { value={args.value ?? 70} color="blue" shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -194,9 +153,8 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 30} color="green" shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -206,9 +164,8 @@ export const Examples: StoryFn = (args) => { color="red" shape="gauge" width={250} - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.2} @@ -218,9 +175,8 @@ export const Examples: StoryFn = (args) => { color="red" width={250} shape="gauge" - gradient="auto" + gradient sparkline={true} - spotlight glowBar={true} glowCenter={true} barWidthFactor={0.8} @@ -231,7 +187,7 @@ export const Examples: StoryFn = (args) => { = (args) => { = (args) => { = (args) => { = (args) => { value={args.value ?? 80} width={250} colorScheme={FieldColorModeId.ContinuousGrYlRd} - spotlight shape="gauge" - gradient="auto" + gradient glowBar={true} glowCenter={true} segmentCount={40} @@ -285,10 +239,9 @@ export const Examples: StoryFn = (args) => { @@ -296,7 +249,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -307,7 +260,7 @@ export const Examples: StoryFn = (args) => { value={args.value ?? 70} width={250} colorScheme={FieldColorModeId.Thresholds} - gradient="auto" + gradient glowCenter={true} thresholdsBar={true} roundedBars={false} @@ -347,14 +300,12 @@ export const Temp: StoryFn = (args) => { shape="gauge" roundedBars={false} barWidthFactor={0.8} - spotlight /> ); }; interface ExampleProps { - gradient?: RadialGradientMode; color?: string; seriesName?: string; value?: number; @@ -363,7 +314,7 @@ interface ExampleProps { max?: number; width?: number; height?: number; - spotlight?: boolean; + gradient?: boolean; glowBar?: boolean; glowCenter?: boolean; barWidthFactor?: number; @@ -376,12 +327,12 @@ interface ExampleProps { roundedBars?: boolean; thresholdsBar?: boolean; colorScheme?: FieldColorModeId; + endpointMarker?: RadialGaugeProps['endpointMarker']; decimals?: number; showScaleLabels?: boolean; } export function RadialGaugeExample({ - gradient = 'none', color, seriesName = 'Server A', value = 70, @@ -390,7 +341,7 @@ export function RadialGaugeExample({ max = 100, width = 200, height = 200, - spotlight = false, + gradient = false, glowBar = false, glowCenter = false, barWidthFactor = 0.4, @@ -403,6 +354,7 @@ export function RadialGaugeExample({ roundedBars = false, thresholdsBar = false, colorScheme = FieldColorModeId.Thresholds, + endpointMarker = 'glow', decimals = 0, showScaleLabels, }: ExampleProps) { @@ -480,7 +432,6 @@ export function RadialGaugeExample({ barWidthFactor={barWidthFactor} gradient={gradient} shape={shape} - spotlight={spotlight} glowBar={glowBar} glowCenter={glowCenter} textMode={textMode} @@ -490,6 +441,7 @@ export function RadialGaugeExample({ roundedBars={roundedBars} thresholdsBar={thresholdsBar} showScaleLabels={showScaleLabels} + endpointMarker={endpointMarker} /> ); } diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx index e2fed36ec3f..783e3b764da 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.test.tsx @@ -1,13 +1,28 @@ import { render, screen } from '@testing-library/react'; +import { ComponentProps } from 'react'; import { RadialGaugeExample } from './RadialGauge.story'; describe('RadialGauge', () => { - it('should render', () => { - render(); - - expect(screen.getByRole('img')).toBeInTheDocument(); - }); + it.each([ + { description: 'default', props: {} }, + { description: 'gauge shape', props: { shape: 'gauge' } }, + { description: 'with gradient', props: { gradient: true } }, + { description: 'with glow bar', props: { glowBar: true } }, + { description: 'with glow center', props: { glowCenter: true } }, + { description: 'with segments', props: { segmentCount: 5 } }, + { description: 'with rounded bars', props: { roundedBars: true } }, + { description: 'with endpoint marker glow', props: { roundedBars: true, endpointMarker: 'glow' } }, + { description: 'with endpoint marker point', props: { roundedBars: true, endpointMarker: 'point' } }, + { description: 'with thresholds bar', props: { thresholdsBar: true } }, + { description: 'with sparkline', props: { sparkline: true } }, + ] satisfies Array<{ description: string; props?: ComponentProps }>)( + 'should render $description without throwing', + ({ props }) => { + render(); + expect(screen.getByRole('img')).toBeInTheDocument(); + } + ); it('should render threshold labels', () => { render(); diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 96e8b0f50f3..452dd394ab1 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -1,8 +1,8 @@ import { css, cx } from '@emotion/css'; -import { isNumber } from 'lodash'; import { useId } from 'react'; -import { DisplayValueAlignmentFactors, FieldDisplay, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data'; +import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; @@ -10,12 +10,13 @@ import { getFormattedThresholds } from '../Gauge/utils'; import { RadialBar } from './RadialBar'; import { RadialBarSegmented } from './RadialBarSegmented'; -import { RadialColorDefs } from './RadialColorDefs'; import { RadialScaleLabels } from './RadialScaleLabels'; import { RadialSparkline } from './RadialSparkline'; import { RadialText } from './RadialText'; import { ThresholdsBar } from './ThresholdsBar'; +import { buildGradientColors } from './colors'; import { GlowGradient, MiddleCircleGlow, SpotlightGradient } from './effects'; +import { RadialShape, RadialTextMode } from './types'; import { calculateDimensions, getValueAngleForValue } from './utils'; export interface RadialGaugeProps { @@ -26,7 +27,7 @@ export interface RadialGaugeProps { * Circle or gauge (partial circle) */ shape?: RadialShape; - gradient?: RadialGradientMode; + gradient?: boolean; /** * Bar width is always relative to size of the gauge. * But this gives you control over the width relative to size. @@ -34,12 +35,14 @@ export interface RadialGaugeProps { * Defaults to 0.4 **/ barWidthFactor?: number; - /** Adds a white spotlight for the end position */ - spotlight?: boolean; glowBar?: boolean; glowCenter?: boolean; roundedBars?: boolean; thresholdsBar?: boolean; + /** + * Specify if an endpoint marker should be shown at the end of the bar + */ + endpointMarker?: 'point' | 'glow'; /** * Number of segments depends on size of gauge but this * factor 1-10 gives you relative control @@ -66,12 +69,9 @@ export interface RadialGaugeProps { showScaleLabels?: boolean; /** For data links */ onClick?: React.MouseEventHandler; + timeRange?: TimeRange; } -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 */ @@ -80,9 +80,8 @@ export function RadialGauge(props: RadialGaugeProps) { width = 256, height = 256, shape = 'circle', - gradient = 'none', + gradient = false, barWidthFactor = 0.4, - spotlight = false, glowBar = false, glowCenter = false, textMode = 'auto', @@ -92,6 +91,7 @@ export function RadialGauge(props: RadialGaugeProps) { roundedBars = true, thresholdsBar = false, showScaleLabels = false, + endpointMarker, onClick, values, } = props; @@ -99,6 +99,11 @@ export function RadialGauge(props: RadialGaugeProps) { const gaugeId = useId(); const styles = useStyles2(getStyles); + let effectiveTextMode = textMode; + if (effectiveTextMode === 'auto') { + effectiveTextMode = vizCount === 1 ? 'value' : 'value_and_name'; + } + const startAngle = shape === 'gauge' ? 250 : 0; const endAngle = shape === 'gauge' ? 110 : 360; @@ -109,7 +114,8 @@ export function RadialGauge(props: RadialGaugeProps) { for (let barIndex = 0; barIndex < values.length; barIndex++) { const displayValue = values[barIndex]; const { angle, angleRange } = getValueAngleForValue(displayValue, startAngle, endAngle); - const color = displayValue.display.color ?? 'gray'; + const gradientStops = buildGradientColors(gradient, theme, displayValue); + const color = displayValue.display.color ?? FALLBACK_COLOR; const dimensions = calculateDimensions( width, height, @@ -122,20 +128,12 @@ export function RadialGauge(props: RadialGaugeProps) { showScaleLabels ); - const displayProcessor = getFieldDisplayProcessor(displayValue); + // FIXME: I want to move the ids for these filters into a context which the children + // can reference via a hook, rather than passing them down as props const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`; const glowFilterId = `glow-${gaugeId}`; - const colorDefs = new RadialColorDefs({ - gradient, - fieldDisplay: displayValue, - theme, - dimensions, - shape, - gaugeId, - displayProcessor, - }); - if (spotlight && theme.isDark) { + if (endpointMarker === 'glow') { defs.push( ); } else { @@ -167,13 +166,16 @@ export function RadialGauge(props: RadialGaugeProps) { ); } @@ -181,7 +183,7 @@ export function RadialGauge(props: RadialGaugeProps) { // These elements are only added for first value / bar if (barIndex === 0) { if (glowBar) { - defs.push(); + defs.push(); } if (glowCenter) { @@ -191,14 +193,14 @@ export function RadialGauge(props: RadialGaugeProps) { graphics.push( ); @@ -233,7 +235,8 @@ export function RadialGauge(props: RadialGaugeProps) { angleRange={angleRange} roundedBars={roundedBars} glowFilter={`url(#${glowFilterId})`} - colorDefs={colorDefs} + shape={shape} + gradient={gradientStops} /> ); } @@ -247,6 +250,7 @@ export function RadialGauge(props: RadialGaugeProps) { theme={theme} color={color} shape={shape} + textMode={effectiveTextMode} /> ); } @@ -272,23 +276,16 @@ export function RadialGauge(props: RadialGaugeProps) { } return ( -
+
{body}
); } -function getFieldDisplayProcessor(displayValue: FieldDisplay) { - if (displayValue.view && isNumber(displayValue.colIndex)) { - const dp = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex); - if (dp) { - return dp; - } - } - - return getDisplayProcessor(); -} - function getStyles(theme: GrafanaTheme2) { return { vizWrapper: css({ diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx index 994b3b35eac..6588150602a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialScaleLabels.tsx @@ -1,87 +1,84 @@ +import { memo } from 'react'; + import { FieldDisplay, GrafanaTheme2, Threshold } from '@grafana/data'; import { t } from '@grafana/i18n'; import { measureText } from '../../utils/measureText'; -import { GaugeDimensions, toCartesian } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { getFieldConfigMinMax, toCartesian } from './utils'; interface RadialScaleLabelsProps { fieldDisplay: FieldDisplay; theme: GrafanaTheme2; thresholds: Threshold[]; - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; startAngle: number; endAngle: number; angleRange: number; } -export function RadialScaleLabels({ - fieldDisplay, - thresholds, - theme, - dimensions, - startAngle, - endAngle, - angleRange, -}: RadialScaleLabelsProps) { - const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; +const LINE_HEIGHT_FACTOR = 1.2; - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; +export const RadialScaleLabels = memo( + ({ fieldDisplay, thresholds, theme, dimensions, startAngle, endAngle, angleRange }: RadialScaleLabelsProps) => { + const { centerX, centerY, scaleLabelsFontSize, scaleLabelsRadius } = dimensions; + const [min, max] = getFieldConfigMinMax(fieldDisplay); - const fontSize = scaleLabelsFontSize; - const textLineHeight = scaleLabelsFontSize * 1.2; - const radius = scaleLabelsRadius - textLineHeight; + const fontSize = scaleLabelsFontSize; + const textLineHeight = scaleLabelsFontSize * LINE_HEIGHT_FACTOR; + const radius = scaleLabelsRadius - textLineHeight; - function getTextPosition(text: string, value: number, index: number) { - const isLast = index === thresholds.length - 1; - const isFirst = index === 0; + function getTextPosition(text: string, value: number, index: number) { + const isLast = index === thresholds.length - 1; + const isFirst = index === 0; - let valueDeg = ((value - min) / (max - min)) * angleRange; - let finalAngle = startAngle + valueDeg; + let valueDeg = ((value - min) / (max - min)) * angleRange; + let finalAngle = startAngle + valueDeg; - // Now adjust the final angle based on the label text width and the labels position on the arc - let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); - let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; + // Now adjust the final angle based on the label text width and the labels position on the arc + let measure = measureText(text, fontSize, theme.typography.fontWeightMedium); + let textWidthAngle = (measure.width / (2 * Math.PI * radius)) * angleRange; - // the centering is different for gauge or circle shapes for some reason - finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; + // the centering is different for gauge or circle shapes for some reason + finalAngle -= endAngle < 180 ? textWidthAngle : textWidthAngle / 2; - // For circle gauges we need to shift the first label more - if (isFirst) { - finalAngle += textWidthAngle; + // For circle gauges we need to shift the first label more + if (isFirst) { + finalAngle += textWidthAngle; + } + + // For circle gauges we need to shift the last label more + if (isLast && endAngle === 360) { + finalAngle -= textWidthAngle; + } + + const position = toCartesian(centerX, centerY, radius, finalAngle); + + return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; } - // For circle gauges we need to shift the last label more - if (isLast && endAngle === 360) { - finalAngle -= textWidthAngle; - } - - const position = toCartesian(centerX, centerY, radius, finalAngle); - - return { ...position, transform: `rotate(${finalAngle}, ${position.x}, ${position.y})` }; + return ( + + {thresholds.map((threshold, index) => { + const labelPos = getTextPosition(String(threshold.value), threshold.value, index); + return ( + + {threshold.value} + + ); + })} + + ); } +); - return ( - - {thresholds.map((threshold, index) => { - const labelPos = getTextPosition(String(threshold.value), threshold.value, index); - - return ( - - {threshold.value} - - ); - })} - - ); -} +RadialScaleLabels.displayName = 'RadialScaleLabels'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx index 7a25fe3201a..4a52d5241d5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx @@ -1,52 +1,76 @@ -import { css } from '@emotion/css'; +import { memo, useMemo } from 'react'; import { FieldDisplay, GrafanaTheme2, FieldConfig } from '@grafana/data'; import { GraphFieldConfig, GraphGradientMode, LineInterpolation } from '@grafana/schema'; import { Sparkline } from '../Sparkline/Sparkline'; -import { RadialShape } from './RadialGauge'; -import { GaugeDimensions } from './utils'; +import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialSparklineProps { - sparkline: FieldDisplay['sparkline']; - dimensions: GaugeDimensions; - theme: GrafanaTheme2; color?: string; - shape?: RadialShape; + dimensions: RadialGaugeDimensions; + shape: RadialShape; + sparkline: FieldDisplay['sparkline']; + textMode: Exclude; + theme: GrafanaTheme2; } -export function RadialSparkline({ sparkline, dimensions, theme, color, shape }: RadialSparklineProps) { - if (!sparkline) { - return null; + +const SPARKLINE_HEIGHT_DIVISOR = 4; +const SPARKLINE_HEIGHT_DIVISOR_NAME_AND_VALUE = 4; +const SPARKLINE_WIDTH_FACTOR_ARC = 1.4; +const SPARKLINE_WIDTH_FACTOR_CIRCLE = 1.6; +const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE = 4; +const SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE = 3.3; +const SPARKLINE_SPACING = 8; + +export function getSparklineDimensions( + radius: number, + barWidth: number, + showNameAndValue: boolean, + shape: RadialShape +): { width: number; height: number } { + const height = radius / (showNameAndValue ? SPARKLINE_HEIGHT_DIVISOR_NAME_AND_VALUE : SPARKLINE_HEIGHT_DIVISOR); + const width = radius * (shape === 'gauge' ? SPARKLINE_WIDTH_FACTOR_ARC : SPARKLINE_WIDTH_FACTOR_CIRCLE) - barWidth; + return { width, height }; +} + +export const RadialSparkline = memo( + ({ sparkline, dimensions, theme, color, shape, textMode }: RadialSparklineProps) => { + const { radius, barWidth } = dimensions; + + const showNameAndValue = textMode === 'value_and_name'; + const { width, height } = getSparklineDimensions(radius, barWidth, showNameAndValue, shape); + const topPos = + shape === 'gauge' + ? dimensions.gaugeBottomY - height - SPARKLINE_SPACING + : `calc(50% + ${radius / (showNameAndValue ? SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE_NAME_AND_VALUE : SPARKLINE_TOP_OFFSET_DIVISOR_CIRCLE)}px)`; + + const config: FieldConfig = useMemo( + () => ({ + color: { + mode: 'fixed', + fixedColor: color ?? 'blue', + }, + custom: { + gradientMode: GraphGradientMode.Opacity, + fillOpacity: 40, + lineInterpolation: LineInterpolation.Smooth, + }, + }), + [color] + ); + + if (!sparkline) { + return null; + } + + return ( +
+ +
+ ); } +); - const { radius, barWidth } = dimensions; - - const height = radius / 4; - const widthFactor = shape === 'gauge' ? 1.6 : 1.4; - const width = radius * widthFactor - barWidth; - const topPos = shape === 'gauge' ? `${dimensions.gaugeBottomY - height}px` : `calc(50% + ${radius / 2.8}px)`; - - const styles = css({ - position: 'absolute', - top: topPos, - }); - - const config: FieldConfig = { - color: { - mode: 'fixed', - fixedColor: color ?? 'blue', - }, - custom: { - gradientMode: GraphGradientMode.Opacity, - fillOpacity: 40, - lineInterpolation: LineInterpolation.Smooth, - }, - }; - - return ( -
- -
- ); -} +RadialSparkline.displayName = 'RadialSparkline'; diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index d01a2d99570..69ab16e450e 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -1,151 +1,162 @@ import { css } from '@emotion/css'; +import { memo } from 'react'; -import { DisplayValue, DisplayValueAlignmentFactors, formattedValueToString, GrafanaTheme2 } from '@grafana/data'; +import { + DisplayValue, + DisplayValueAlignmentFactors, + FieldSparkline, + formattedValueToString, + GrafanaTheme2, +} from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; import { calculateFontSize } from '../../utils/measureText'; -import { RadialShape, RadialTextMode } from './RadialGauge'; -import { GaugeDimensions } from './utils'; - -// function toCartesian(centerX: number, centerY: number, radius: number, angleInDegrees: number) { -// let radian = ((angleInDegrees - 90) * Math.PI) / 180.0; -// return { -// x: centerX + radius * Math.cos(radian), -// y: centerY + radius * Math.sin(radian), -// }; -// } +import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types'; interface RadialTextProps { displayValue: DisplayValue; theme: GrafanaTheme2; - dimensions: GaugeDimensions; - textMode: RadialTextMode; - vizCount: number; + dimensions: RadialGaugeDimensions; + textMode: Exclude; shape: RadialShape; + sparkline?: FieldSparkline; alignmentFactors?: DisplayValueAlignmentFactors; valueManualFontSize?: number; nameManualFontSize?: number; } -export function RadialText({ - displayValue, - theme, - dimensions, - textMode, - vizCount, - shape, - alignmentFactors, - valueManualFontSize, - nameManualFontSize, -}: RadialTextProps) { - const styles = useStyles2(getStyles); - const { centerX, centerY, radius, barWidth } = dimensions; +const LINE_HEIGHT_FACTOR = 1.21; +const VALUE_WIDTH_TO_RADIUS_FACTOR = 0.82; +const NAME_TO_HEIGHT_FACTOR = 0.45; +const LARGE_RADIUS_SCALING_DECAY = 0.86; +const MAX_TEXT_WIDTH_DIVISOR = 7; +const MAX_NAME_HEIGHT_DIVISOR = 4; +const VALUE_SPACE_PERCENTAGE = 0.7; +const SPARKLINE_SPACING = 8; +const MIN_VALUE_FONT_SIZE = 1; +const MIN_NAME_FONT_SIZE = 10; +const MIN_UNIT_FONT_SIZE = 6; - if (textMode === 'none') { - return null; - } +export const RadialText = memo( + ({ + displayValue, + theme, + dimensions, + textMode, + shape, + sparkline, + alignmentFactors, + valueManualFontSize, + nameManualFontSize, + }: RadialTextProps) => { + const styles = useStyles2(getStyles); + const { centerX, centerY, radius, barWidth } = dimensions; - if (textMode === 'auto') { - textMode = vizCount === 1 ? 'value' : 'value_and_name'; - } + if (textMode === 'none') { + return null; + } - const nameToAlignTo = (alignmentFactors ? alignmentFactors.title : displayValue.title) ?? ''; - const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : displayValue); + const nameToAlignTo = (alignmentFactors ? alignmentFactors.title : displayValue.title) ?? ''; + const valueToAlignTo = formattedValueToString(alignmentFactors ? alignmentFactors : displayValue); - const showValue = textMode === 'value' || textMode === 'value_and_name'; - const showName = textMode === 'name' || textMode === 'value_and_name'; - const maxTextWidth = radius * 2 - barWidth - radius / 7; + const showValue = textMode === 'value' || textMode === 'value_and_name'; + const showName = textMode === 'name' || textMode === 'value_and_name'; + const maxTextWidth = radius * 2 - barWidth - radius / MAX_TEXT_WIDTH_DIVISOR; - // Not sure where this comes from but svg text is not using body line-height - const lineHeight = 1.21; - const valueWidthToRadiusFactor = 0.85; - const nameToHeightFactor = 0.45; - const largeRadiusScalingDecay = 0.86; + // This pow 0.92 factor is to create a decay so the font size does not become rediculously large for very large panels + let maxValueHeight = VALUE_WIDTH_TO_RADIUS_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + let maxNameHeight = radius / MAX_NAME_HEIGHT_DIVISOR; - // This pow 0.92 factor is to create a decay so the font size does not become rediculously large for very large panels - let maxValueHeight = valueWidthToRadiusFactor * Math.pow(radius, largeRadiusScalingDecay); - let maxNameHeight = radius / 4; + if (showValue && showName) { + maxValueHeight = VALUE_WIDTH_TO_RADIUS_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + maxNameHeight = NAME_TO_HEIGHT_FACTOR * Math.pow(radius, LARGE_RADIUS_SCALING_DECAY); + } - if (showValue && showName) { - maxValueHeight = valueWidthToRadiusFactor * Math.pow(radius, largeRadiusScalingDecay); - maxNameHeight = nameToHeightFactor * Math.pow(radius, largeRadiusScalingDecay); - } - - const valueFontSize = - valueManualFontSize ?? - calculateFontSize( - valueToAlignTo, - maxTextWidth, - maxValueHeight, - lineHeight, - undefined, - theme.typography.body.fontWeight + const valueFontSize = Math.max( + valueManualFontSize ?? + calculateFontSize( + valueToAlignTo, + maxTextWidth, + maxValueHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_VALUE_FONT_SIZE ); - const nameFontSize = - nameManualFontSize ?? - calculateFontSize( - nameToAlignTo, - maxTextWidth, - maxNameHeight, - lineHeight, - undefined, - theme.typography.body.fontWeight + const nameFontSize = Math.max( + nameManualFontSize ?? + calculateFontSize( + nameToAlignTo, + maxTextWidth, + maxNameHeight, + LINE_HEIGHT_FACTOR, + undefined, + theme.typography.body.fontWeight + ), + MIN_NAME_FONT_SIZE ); - const unitFontSize = Math.max(valueFontSize * 0.7, 5); - const valueHeight = valueFontSize * lineHeight; - const nameHeight = nameFontSize * lineHeight; + const unitFontSize = Math.max(valueFontSize * VALUE_SPACE_PERCENTAGE, MIN_UNIT_FONT_SIZE); + const valueHeight = valueFontSize * LINE_HEIGHT_FACTOR; + const nameHeight = nameFontSize * LINE_HEIGHT_FACTOR; - const valueY = showName ? centerY - nameHeight / 2 : centerY; - const valueNameSpacing = valueHeight / 3.5; - const nameY = showValue ? valueY + valueHeight / 2 + valueNameSpacing : centerY; - const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary; - const suffixShift = (valueFontSize - unitFontSize * 1.2) / 2; + const valueY = showName ? centerY - nameHeight * (1 - VALUE_SPACE_PERCENTAGE) : centerY; + const nameY = showValue ? valueY + valueHeight * VALUE_SPACE_PERCENTAGE : centerY; + const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary; + const suffixShift = (valueFontSize - unitFontSize * LINE_HEIGHT_FACTOR) / 2; - // For gauge shape we shift text up a bit - const valueDy = shape === 'gauge' ? -valueFontSize * 0.3 : 0; - const nameDy = shape === 'gauge' ? -nameFontSize * 0.7 : 0; + // adjust the text up on gauges and when sparklines are present + let yOffset = 0; + if (shape === 'gauge') { + // we render from the center of the gauge, so move up by half of half of the total height + yOffset -= (valueHeight + nameHeight) / 4; + } + if (sparkline) { + yOffset -= SPARKLINE_SPACING; + } - return ( - - {showValue && ( - - {displayValue.prefix ?? ''} - {displayValue.text} - - {displayValue.suffix ?? ''} - - - )} - {showName && ( - - {displayValue.title} - - )} - - ); -} + return ( + + {showValue && ( + + {displayValue.prefix ?? ''} + {displayValue.text} + + {displayValue.suffix ?? ''} + + + )} + {showName && ( + + {displayValue.title} + + )} + + ); + } +); -const getStyles = (theme: GrafanaTheme2) => ({ +RadialText.displayName = 'RadialText'; + +const getStyles = (_theme: GrafanaTheme2) => ({ text: css({ verticalAlign: 'bottom', }), diff --git a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx index 602038ccf10..cb2829934b9 100644 --- a/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/ThresholdsBar.tsx @@ -1,20 +1,22 @@ import { FieldDisplay, Threshold } from '@grafana/data'; import { RadialArcPath } from './RadialArcPath'; -import { RadialColorDefs } from './RadialColorDefs'; -import { GaugeDimensions } from './utils'; +import { GradientStop, RadialGaugeDimensions, RadialShape } from './types'; +import { getFieldConfigMinMax } from './utils'; -export interface Props { - dimensions: GaugeDimensions; +interface ThresholdsBarProps { + dimensions: RadialGaugeDimensions; angleRange: number; startAngle: number; endAngle: number; + shape: RadialShape; fieldDisplay: FieldDisplay; roundedBars?: boolean; glowFilter?: string; - colorDefs: RadialColorDefs; thresholds: Threshold[]; + gradient?: GradientStop[]; } + export function ThresholdsBar({ dimensions, fieldDisplay, @@ -22,19 +24,18 @@ export function ThresholdsBar({ angleRange, roundedBars, glowFilter, - colorDefs, thresholds, -}: Props) { - const fieldConfig = fieldDisplay.field; - const min = fieldConfig.min ?? 0; - const max = fieldConfig.max ?? 100; - + shape, + gradient, +}: ThresholdsBarProps) { const thresholdDimensions = { ...dimensions, barWidth: dimensions.thresholdsBarWidth, radius: dimensions.thresholdsBarRadius, }; + const [min, max] = getFieldConfigMinMax(fieldDisplay); + let currentStart = startAngle; let paths: React.ReactNode[] = []; @@ -48,27 +49,26 @@ export function ThresholdsBar({ valueDeg = 0; } - let lengthDeg = valueDeg - currentStart + startAngle; + const lengthDeg = valueDeg - currentStart + startAngle; + const colorProps = gradient ? { gradient } : { color: threshold.color }; paths.push( ); currentStart += lengthDeg; } - return ( - <> - {paths} - {colorDefs.getDefs()} - - ); + return {paths}; } diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap new file mode 100644 index 00000000000..97b053c2d61 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/colors.test.ts.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadialGauge color utils buildGradientColors should map threshold colors correctly (with baseColor if displayProcessor does not return colors) 1`] = ` +[ + { + "color": "#444444", + "percent": 0, + }, + { + "color": "#FADE2A", + "percent": 0.5, + }, + { + "color": "#F2495C", + "percent": 0.8, + }, + { + "color": "#444444", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should map threshold colors correctly (with baseColor if displayProcessor does not return colors) 2`] = ` +[ + { + "color": "#FF0000", + "percent": 0, + }, + { + "color": "#FADE2A", + "percent": 0.5, + }, + { + "color": "#F2495C", + "percent": 0.8, + }, + { + "color": "#FF0000", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for by-value color mode in dark theme 1`] = ` +[ + { + "color": "#181b1f", + "percent": 0, + }, + { + "color": "#1F60C4", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for by-value color mode in light theme 1`] = ` +[ + { + "color": "#ffffff", + "percent": 0, + }, + { + "color": "#1250B0", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for continuous color modes 1`] = ` +[ + { + "color": "rgb(0, 32, 81)", + "percent": 0, + }, + { + "color": "rgb(17, 54, 108)", + "percent": 0.125, + }, + { + "color": "rgb(60, 77, 110)", + "percent": 0.25, + }, + { + "color": "rgb(98, 100, 111)", + "percent": 0.375, + }, + { + "color": "rgb(127, 124, 117)", + "percent": 0.5, + }, + { + "color": "rgb(154, 148, 120)", + "percent": 0.625, + }, + { + "color": "rgb(187, 175, 113)", + "percent": 0.75, + }, + { + "color": "rgb(226, 203, 92)", + "percent": 0.875, + }, + { + "color": "rgb(253, 234, 69)", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in dark theme 1`] = ` +[ + { + "color": "#37237a", + "percent": 0, + }, + { + "color": "#a146da", + "percent": 0.75, + }, + { + "color": "#a146da", + "percent": 1, + }, +] +`; + +exports[`RadialGauge color utils buildGradientColors should return gradient colors for fixed color mode in light theme 1`] = ` +[ + { + "color": "#a146da", + "percent": 0, + }, + { + "color": "#3e2b9a", + "percent": 0.75, + }, + { + "color": "#3e2b9a", + "percent": 1, + }, +] +`; diff --git a/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..db4c1c40882 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/__snapshots__/utils.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 110 A 90 90 0 1 1 149.98429203681178 110.00000137077838 A 10 10 0 0 1 149.98778269529805 130.00000106616096 A 70 70 0 1 0 150 130 A 10 10 0 0 1 150 110 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 10 A 90 90 0 0 1 100 190 L 100 170 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 17.5 A 82.5 82.5 0 0 1 100 182.5 L 100 177.5 A 77.5 77.5 0 0 0 100 22.5 L 100 17.5 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 40 A 60 60 0 0 1 100 160 L 100 140 A 40 40 0 0 0 100 60 L 100 40 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 10 A 90 90 0 0 1 190 100 L 170 100 A 70 70 0 0 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 A 10 10 0 0 1 30 100.00000000000001 A 70 70 0 1 0 100 30 A 10 10 0 0 1 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 L 30 100.00000000000001 A 70 70 0 1 0 100 30 L 100 10 Z"`; + +exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 -5 A 105 105 0 0 1 100 205 L 100 155 A 55 55 0 0 0 100 45 L 100 -5 Z"`; diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.test.ts b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts new file mode 100644 index 00000000000..321e95bb921 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.test.ts @@ -0,0 +1,306 @@ +import { defaultsDeep } from 'lodash'; + +import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { + buildGradientColors, + colorAtGradientPercent, + getBarEndcapColors, + getEndpointMarkerColors, + getGradientCss, + getGradientStopsForPercent, +} from './colors'; + +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +describe('RadialGauge color utils', () => { + describe('buildGradientColors', () => { + const createField = (colorMode: FieldColorModeId): Field => + ({ + type: FieldType.number, + name: 'Test Field', + config: { + color: { + mode: colorMode, + }, + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { value: -Infinity, color: 'green' }, + { value: 50, color: 'yellow' }, + { value: 80, color: 'red' }, + ], + }, + }, + values: [70, 40, 30, 90, 55], + }) satisfies Field; + + const buildFieldDisplay = (field: Field, part = {}): FieldDisplay => + defaultsDeep(part, { + field: field.config, + colIndex: 0, + view: { + getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: undefined }))), + }, + display: { + numeric: 75, + }, + }); + + it('should return the baseColor if gradient is false-y', () => { + expect( + buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') + ).toEqual([ + { color: '#FF0000', percent: 0 }, + { color: '#FF0000', percent: 1 }, + ]); + + expect( + buildGradientColors(undefined, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000') + ).toEqual([ + { color: '#FF0000', percent: 0 }, + { color: '#FF0000', percent: 1 }, + ]); + }); + + it('uses the fallback color if no baseColor is set', () => { + expect(buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)))).toEqual( + [ + { color: FALLBACK_COLOR, percent: 0 }, + { color: FALLBACK_COLOR, percent: 1 }, + ] + ); + }); + + it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { + expect( + buildGradientColors( + true, + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.Thresholds), { + view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) }, + }) + ) + ).toMatchSnapshot(); + }); + + it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => { + expect( + buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000') + ).toMatchSnapshot(); + }); + + it('should return gradient colors for continuous color modes', () => { + expect( + buildGradientColors( + true, + createTheme(), + buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)), + '#00FF00' + ) + ).toMatchSnapshot(); + }); + + it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => { + expect( + buildGradientColors( + true, + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues)) + ) + ).toMatchSnapshot(); + }); + + it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => { + expect( + buildGradientColors( + true, + createTheme({ colors: { mode } }), + buildFieldDisplay(createField(FieldColorModeId.Fixed)), + '#442299' + ) + ).toMatchSnapshot(); + }); + }); + + describe('colorAtGradientPercent', () => { + it('should calculate the color at a given percent in a gradient of two colors', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#bf0040'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#800080'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#4000bf'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('should calculate the color at a given percent in a gradient of multiple colors', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#808000'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#00ff00'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#008080'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('will still work if unsorted', () => { + const gradient = [ + { color: '#0000ff', percent: 1 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#ff0000', percent: 0 }, + ]; + expect(colorAtGradientPercent(gradient, 0).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 0.25).toHexString()).toBe('#808000'); + expect(colorAtGradientPercent(gradient, 0.5).toHexString()).toBe('#00ff00'); + expect(colorAtGradientPercent(gradient, 0.75).toHexString()).toBe('#008080'); + expect(colorAtGradientPercent(gradient, 1).toHexString()).toBe('#0000ff'); + }); + + it('should not throw an error when percent is outside 0-1 range', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#0000ff', percent: 1 }, + ]; + expect(colorAtGradientPercent(gradient, -0.5).toHexString()).toBe('#ff0000'); + expect(colorAtGradientPercent(gradient, 1.5).toHexString()).toBe('#0000ff'); + }); + + it('should throw an error when less than two stops are provided', () => { + expect(() => { + colorAtGradientPercent([], 0.5); + }).toThrow('colorAtGradientPercent requires at least two color stops'); + expect(() => { + colorAtGradientPercent([{ color: '#ff0000', percent: 0 }], 0.5); + }).toThrow('colorAtGradientPercent requires at least two color stops'); + }); + }); + + describe('getBarEndcapColors', () => { + it('should return the first and last colors in the gradient', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const [startColor, endColor] = getBarEndcapColors(gradient); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#0000ff'); + }); + + it('should return the correct end color based on percent', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const [startColor, endColor] = getBarEndcapColors(gradient, 0.25); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#808000'); + }); + + it('should handle gradients with only one colors', () => { + const gradient = [{ color: '#ff0000', percent: 0 }]; + const [startColor, endColor] = getBarEndcapColors(gradient); + expect(startColor).toBe('#ff0000'); + expect(endColor).toBe('#ff0000'); + }); + + it('should throw an error when no colors are provided', () => { + expect(() => { + getBarEndcapColors([]); + }).toThrow('getBarEndcapColors requires at least one color stop'); + }); + }); + + describe('getGradientCss', () => { + it('should return conic-gradient CSS for circle shape', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const css = getGradientCss(gradient, 'circle'); + expect(css).toBe('conic-gradient(from 0deg, #ff0000 0.00%, #00ff00 50.00%, #0000ff 100.00%)'); + }); + + it('should return linear-gradient CSS for arc shape', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const css = getGradientCss(gradient, 'gauge'); + expect(css).toBe('linear-gradient(90deg, #ff0000 0.00%, #00ff00 50.00%, #0000ff 100.00%)'); + }); + }); + + describe('getEndpointMarkerColors', () => { + it('should return contrasting guide dot colors based on the gradient endpoints and percent', () => { + const gradient = [ + { color: '#000000', percent: 0 }, + { color: '#ffffff', percent: 0.5 }, + { color: '#ffffff', percent: 1 }, + ]; + const [startDotColor, endDotColor] = getEndpointMarkerColors(gradient, 0.35); + expect(startDotColor).toBe('#fbfbfb'); + expect(endDotColor).toBe('#111217'); + }); + }); + + describe('getGradientStopsForPercent', () => { + it('should return the correct gradient stops for a given percent', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + const [left, right] = getGradientStopsForPercent(gradient, 0.25); + expect(left).toEqual({ color: '#ff0000', percent: 0 }); + expect(right).toEqual({ color: '#00ff00', percent: 0.5 }); + }); + + it('should handle edge cases where percent is at the boundaries', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + let [left, right] = getGradientStopsForPercent(gradient, 0); + expect(left).toEqual({ color: '#ff0000', percent: 0 }); + expect(right).toEqual({ color: '#ff0000', percent: 0 }); + + [left, right] = getGradientStopsForPercent(gradient, 1); + expect(left).toEqual({ color: '#0000ff', percent: 1 }); + expect(right).toEqual({ color: '#0000ff', percent: 1 }); + }); + + it('should return the same stop if there is one that is equal to the percentage', () => { + const gradient = [ + { color: '#ff0000', percent: 0 }, + { color: '#00ff00', percent: 0.5 }, + { color: '#0000ff', percent: 1 }, + ]; + + let [left, right] = getGradientStopsForPercent(gradient, 0); + expect(left).toEqual({ color: '#ff0000', percent: 0 }); + expect(right).toEqual({ color: '#ff0000', percent: 0 }); + + [left, right] = getGradientStopsForPercent(gradient, 0.5); + expect(left).toEqual({ color: '#00ff00', percent: 0.5 }); + expect(right).toEqual({ color: '#00ff00', percent: 0.5 }); + + [left, right] = getGradientStopsForPercent(gradient, 1); + expect(left).toEqual({ color: '#0000ff', percent: 1 }); + expect(right).toEqual({ color: '#0000ff', percent: 1 }); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/RadialGauge/colors.ts b/packages/grafana-ui/src/components/RadialGauge/colors.ts new file mode 100644 index 00000000000..61160a9f826 --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/colors.ts @@ -0,0 +1,195 @@ +import tinycolor from 'tinycolor2'; + +import { colorManipulator, FALLBACK_COLOR, FieldDisplay, getFieldColorMode, GrafanaTheme2 } from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { GradientStop, RadialShape } from './types'; +import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils'; + +export function buildGradientColors( + gradient = false, + theme: GrafanaTheme2, + fieldDisplay: FieldDisplay, + baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR +): GradientStop[] { + if (!gradient) { + return [ + { color: baseColor, percent: 0 }, + { color: baseColor, percent: 1 }, + ]; + } + + const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode); + + // thresholds get special handling + if (colorMode.id === FieldColorModeId.Thresholds) { + const displayProcessor = getFieldDisplayProcessor(fieldDisplay); + const [min, max] = getFieldConfigMinMax(fieldDisplay); + const thresholds = fieldDisplay.field.thresholds?.steps ?? []; + + const result: Array<{ color: string; percent: number }> = [ + { color: displayProcessor(min).color ?? baseColor, percent: 0 }, + ]; + + for (const threshold of thresholds) { + if (threshold.value > min && threshold.value < max) { + const percent = (threshold.value - min) / (max - min); + result.push({ color: theme.visualization.getColorByName(threshold.color), percent }); + } + } + + result.push({ color: displayProcessor(max).color ?? baseColor, percent: 1 }); + + return result; + } + + // Handle continuous color modes before other by-value modes + if (colorMode.isContinuous && colorMode.getColors) { + const colors = colorMode.getColors(theme); + return colors.map((color, idx) => ({ color, percent: idx / (colors.length - 1) })); + } + + // For value-based colors, we want to stay more true to the specific color, + // so a radial gradient that adds a bit of light and shade works best + if (colorMode.isByValue) { + const darkerColor = tinycolor(baseColor).darken(5); + const lighterColor = tinycolor(baseColor).spin(20).lighten(10); + + const color1 = theme.isDark ? lighterColor : darkerColor; + const color2 = theme.isDark ? darkerColor : lighterColor; + + return [ + { color: color1.toString(), percent: 0 }, + { color: color2.toString(), percent: 0.6 }, + { color: color2.toString(), percent: 1 }, + ]; + } + + // For fixed / palette based color scales we can create a more hue and light + // based linear gradient that we rotate with the value + const darkerColor = tinycolor(baseColor) + .spin(-20) + .darken(theme.isDark ? 15 : 5); + const lighterColor = tinycolor(baseColor).saturate(20).spin(20).brighten(10).lighten(10); + + const underlyingGradient = [ + { color: theme.isDark ? darkerColor.toString() : lighterColor.toString(), percent: 0 }, + { color: theme.isDark ? lighterColor.toString() : darkerColor.toString(), percent: 1 }, + ]; + + // rotate the gradient so that the highest contrasting point is the value, depending on theme. + const valuePercent = getValuePercentageForValue(fieldDisplay); + const startColor = theme.isDark + ? colorAtGradientPercent(underlyingGradient, 1 - valuePercent).toHexString() + : underlyingGradient[0].color; + const endColor = theme.isDark + ? underlyingGradient[1].color + : colorAtGradientPercent(underlyingGradient, valuePercent).toHexString(); + return [ + { color: startColor, percent: 0 }, + { color: endColor, percent: valuePercent }, + { color: endColor, percent: 1 }, + ]; +} + +/** + * get the relevant gradient stops surrounding a given percentage. could be same stop if the + * percent matches a stop exactly. + * + * @param sortedGradientStops - gradient stops sorted by percent + * @param percent - percentage 0..1 + * @returns {[GradientStop, GradientStop]} - the two gradient stops surrounding the given percentage + */ +export function getGradientStopsForPercent( + sortedGradientStops: GradientStop[], + percent: number +): [GradientStop, GradientStop] { + if (percent <= 0) { + return [sortedGradientStops[0], sortedGradientStops[0]]; + } + if (percent >= 1) { + const last = sortedGradientStops.length - 1; + return [sortedGradientStops[last], sortedGradientStops[last]]; + } + + // find surrounding stops using binary search + let lo = 0; + let hi = sortedGradientStops.length - 1; + while (lo + 1 < hi) { + const mid = (lo + hi) >> 1; + if (percent === sortedGradientStops[mid].percent) { + return [sortedGradientStops[mid], sortedGradientStops[mid]]; + } + + if (percent < sortedGradientStops[mid].percent) { + hi = mid; + } else { + lo = mid; + } + } + return [sortedGradientStops[lo], sortedGradientStops[hi]]; +} + +/** + * @alpha - perhaps this should go in colorManipulator.ts + * Given color stops (each with a color and percentage 0..1) returns the color at a given percentage. + * Uses tinycolor.mix for interpolation. + * @params stops - array of color stops (percentages 0..1) + * @params percent - percentage 0..1 + * @returns color at the given percentage + */ +export function colorAtGradientPercent(stops: GradientStop[], percent: number): tinycolor.Instance { + if (!stops || stops.length < 2) { + throw new Error('colorAtGradientPercent requires at least two color stops'); + } + + const sorted = stops + .map((s: GradientStop): GradientStop => ({ color: s.color, percent: Math.min(Math.max(0, s.percent), 1) })) + .sort((a: GradientStop, b: GradientStop) => a.percent - b.percent); + + const [left, right] = getGradientStopsForPercent(sorted, percent); + const range = right.percent - left.percent; + const t = range === 0 ? 0 : (percent - left.percent) / range; // 0..1 + return tinycolor.mix(left.color, right.color, t * 100); +} + +export function getBarEndcapColors(gradientStops: GradientStop[], percent = 1): [string, string] { + if (gradientStops.length === 0) { + throw new Error('getBarEndcapColors requires at least one color stop'); + } + + const startColor = gradientStops[0].color; + let endColor = gradientStops[gradientStops.length - 1].color; + + // if we have a percentageFilled, use it to get a the correct end color based on where the bar terminates + if (gradientStops.length >= 2) { + const endColorByPercentage = colorAtGradientPercent(gradientStops, percent); + endColor = + endColorByPercentage.getAlpha() === 1 ? endColorByPercentage.toHexString() : endColorByPercentage.toHex8String(); + } + return [startColor, endColor]; +} + +export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape): string { + const colorStrings = gradientStops.map((stop) => `${stop.color} ${(stop.percent * 100).toFixed(2)}%`); + if (shape === 'circle') { + return `conic-gradient(from 0deg, ${colorStrings.join(', ')})`; + } + return `linear-gradient(90deg, ${colorStrings.join(', ')})`; +} + +// the theme does not make the full palette available to us, and we +// don't want transparent colors which our grays usually have. +const GRAY_05 = '#111217'; +const GRAY_90 = '#fbfbfb'; +const CONTRAST_THRESHOLD_MAX = 4.5; +const getGuideDotColor = (color: string): string => { + const darkColor = GRAY_05; + const lightColor = GRAY_90; + return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor; +}; + +export function getEndpointMarkerColors(gradientStops: GradientStop[], percent = 1): [string, string] { + const [startColor, endColor] = getBarEndcapColors(gradientStops, percent); + return [getGuideDotColor(startColor), getGuideDotColor(endColor)]; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 76f15900a14..53a255d4a45 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -1,14 +1,18 @@ -import { GrafanaTheme2 } from '@grafana/data'; +import { colorManipulator, GrafanaTheme2 } from '@grafana/data'; -import { GaugeDimensions } from './utils'; +import { RadialGaugeDimensions } from './types'; export interface GlowGradientProps { id: string; - radius: number; + barWidth: number; } -export function GlowGradient({ id, radius }: GlowGradientProps) { - const glowSize = 0.03 * radius; +const MIN_GLOW_SIZE = 0.75; +const GLOW_FACTOR = 0.08; + +export function GlowGradient({ id, barWidth }: GlowGradientProps) { + // 0.75 is the minimum glow size, and it scales with bar width + const glowSize = MIN_GLOW_SIZE + barWidth * GLOW_FACTOR; return ( @@ -21,69 +25,34 @@ export function GlowGradient({ id, radius }: GlowGradientProps) { ); } -export function SpotlightGradient({ - id, - dimensions, - roundedBars, - angle, - theme, -}: { - id: string; - dimensions: GaugeDimensions; - angle: number; - roundedBars: boolean; - theme: GrafanaTheme2; -}) { - const angleRadian = ((angle - 90) * Math.PI) / 180; - - let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); - let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); - let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); - let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); - - if (theme.isLight) { - return ( - - - - - - ); - } - - return ( - - - - {roundedBars && } - - ); -} +const CENTER_GLOW_OPACITY = 0.25; export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) { + const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY); return ( - - - + + + ); } export interface CenterGlowProps { - dimensions: GaugeDimensions; + dimensions: RadialGaugeDimensions; gaugeId: string; color?: string; } export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps) { const gradientId = `circle-glow-${gaugeId}`; + const transparentColor = color ? colorManipulator.alpha(color, CENTER_GLOW_OPACITY) : color; return ( <> - - - + + + @@ -92,3 +61,36 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps ); } + +export function SpotlightGradient({ + id, + dimensions, + roundedBars, + angle, + theme, +}: { + id: string; + dimensions: RadialGaugeDimensions; + angle: number; + roundedBars: boolean; + theme: GrafanaTheme2; +}) { + if (theme.isLight) { + return null; + } + + const angleRadian = ((angle - 90) * Math.PI) / 180; + + let x1 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian - 0.2); + let y1 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian - 0.2); + let x2 = dimensions.centerX + dimensions.radius * Math.cos(angleRadian); + let y2 = dimensions.centerY + dimensions.radius * Math.sin(angleRadian); + + return ( + + + + {roundedBars && } + + ); +} diff --git a/packages/grafana-ui/src/components/RadialGauge/types.ts b/packages/grafana-ui/src/components/RadialGauge/types.ts new file mode 100644 index 00000000000..cc233dd524c --- /dev/null +++ b/packages/grafana-ui/src/components/RadialGauge/types.ts @@ -0,0 +1,25 @@ +export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; +export type RadialShape = 'circle' | 'gauge'; + +export interface RadialGaugeDimensions { + margin: number; + radius: number; + centerX: number; + centerY: number; + barWidth: number; + endAngle?: number; + barIndex: number; + thresholdsBarRadius: number; + thresholdsBarWidth: number; + thresholdsBarSpacing: number; + scaleLabelsFontSize: number; + scaleLabelsSpacing: number; + scaleLabelsRadius: number; + gaugeBottomY: number; +} + +/** @alpha - perhaps this should go in @grafana/data */ +export interface GradientStop { + color: string; + percent: number; +} diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts index 5e3f34d62cd..b9b2e4ad8f3 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.test.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.test.ts @@ -1,24 +1,111 @@ -import { FieldDisplay } from '@grafana/data'; +import { DataFrameView, FieldDisplay } from '@grafana/data'; import type { RadialGaugeProps } from './RadialGauge'; -import { calculateDimensions, toRad, getValueAngleForValue } from './utils'; +import { RadialGaugeDimensions } from './types'; +import { + calculateDimensions, + toRad, + getValueAngleForValue, + drawRadialArcPath, + getFieldConfigMinMax, + getFieldDisplayProcessor, + getAngleBetweenSegments, + getOptimalSegmentCount, +} from './utils'; describe('RadialGauge utils', () => { - function calc(overrides: Partial = {}) { - return calculateDimensions( - overrides.width ?? 200, - overrides.height ?? 200, - overrides.shape === 'gauge' ? 110 : 360, - overrides.glowBar ?? false, - overrides.roundedBars ?? false, - overrides.barWidthFactor ?? 0.4, - overrides.barIndex ?? 0, - overrides.thresholdsBar ?? false, - overrides.showScaleLabels ?? false - ); - } + describe('getFieldDisplayProcessor', () => { + it('should return display processor from view when available', () => { + const mockProcessor = jest.fn(); + const mockView = { + getFieldDisplayProcessor: jest.fn().mockReturnValue(mockProcessor), + } as unknown as DataFrameView; + + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: mockView, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBe(mockProcessor); + expect(mockView.getFieldDisplayProcessor).toHaveBeenCalledWith(0); + }); + + it('should return default display processor when view is not available', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const dp = getFieldDisplayProcessor(fieldDisplay); + expect(dp).toBeDefined(); + expect(typeof dp).toBe('function'); + }); + }); + + describe('getFieldConfigMinMax', () => { + it('should return min and max from field config when defined', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: { min: 10, max: 90 }, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(10); + expect(max).toBe(90); + }); + + it('should return default min and max when not defined in field config', () => { + const fieldDisplay: FieldDisplay = { + display: { numeric: 50, text: '50', color: 'blue' }, + field: {}, + view: undefined, + colIndex: 0, + rowIndex: 0, + name: 'test', + getLinks: () => [], + hasLinks: false, + }; + + const [min, max] = getFieldConfigMinMax(fieldDisplay); + expect(min).toBe(0); + expect(max).toBe(100); + }); + }); describe('calculateDimensions', () => { + function calc(overrides: Partial = {}) { + return calculateDimensions( + overrides.width ?? 200, + overrides.height ?? 200, + overrides.shape === 'gauge' ? 110 : 360, + overrides.glowBar ?? false, + overrides.roundedBars ?? false, + overrides.barWidthFactor ?? 0.4, + overrides.barIndex ?? 0, + overrides.thresholdsBar ?? false, + overrides.showScaleLabels ?? false + ); + } + it('should calculate basic dimensions for a square gauge', () => { const result = calc(); @@ -194,4 +281,84 @@ describe('RadialGauge utils', () => { expect(result.angle).toBe(240); }); }); + + describe('drawRadialArcPath', () => { + const defaultDims: RadialGaugeDimensions = Object.freeze({ + centerX: 100, + centerY: 100, + radius: 80, + barWidth: 20, + margin: 0, + barIndex: 0, + thresholdsBarWidth: 0, + thresholdsBarSpacing: 0, + thresholdsBarRadius: 0, + scaleLabelsFontSize: 0, + scaleLabelsSpacing: 0, + scaleLabelsRadius: 0, + gaugeBottomY: 0, + }); + + it.each([ + { description: 'quarter arc', startAngle: 0, endAngle: 90 }, + { description: 'half arc', startAngle: 0, endAngle: 180 }, + { description: 'three quarter arc', startAngle: 0, endAngle: 270 }, + { description: 'rounded bars', startAngle: 0, endAngle: 270, roundedBars: true }, + { description: 'wide bar width', startAngle: 0, endAngle: 180, dimensions: { barWidth: 50 } }, + { description: 'narrow bar width', startAngle: 0, endAngle: 180, dimensions: { barWidth: 5 } }, + { description: 'narrow radius', startAngle: 0, endAngle: 180, dimensions: { radius: 50 } }, + { + description: 'center x and y', + startAngle: 0, + endAngle: 360, + roundedBars: true, + dimensions: { centerX: 150, centerY: 200 }, + }, + ])(`should draw correct path for $description`, ({ startAngle, endAngle, dimensions, roundedBars }) => { + const path = drawRadialArcPath(startAngle, endAngle, { ...defaultDims, ...dimensions }, roundedBars); + expect(path).toMatchSnapshot(); + }); + + describe('edge cases', () => { + it('should adjust 360deg or greater arcs to avoid SVG rendering issues', () => { + expect(drawRadialArcPath(0, 360, defaultDims)).toEqual(drawRadialArcPath(0, 359.99, defaultDims)); + expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims)); + }); + + it('should return empty string if inner radius collapses to zero or below', () => { + const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 }; + expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe(''); + }); + }); + }); + + describe('getAngleBetweenSegments', () => { + it('should calculate angle between segments based on spacing and count', () => { + expect(getAngleBetweenSegments(2, 10, 360)).toBe(48); + expect(getAngleBetweenSegments(5, 15, 180)).toBe(40); + }); + }); + + describe('getOptimalSegmentCount', () => { + it('should adjust segment count based on dimensions and spacing', () => { + const dimensions: RadialGaugeDimensions = { + centerX: 100, + centerY: 100, + radius: 80, + barWidth: 20, + margin: 0, + barIndex: 0, + thresholdsBarWidth: 0, + thresholdsBarSpacing: 0, + thresholdsBarRadius: 0, + scaleLabelsFontSize: 0, + scaleLabelsSpacing: 0, + scaleLabelsRadius: 0, + gaugeBottomY: 0, + }; + + expect(getOptimalSegmentCount(dimensions, 2, 10, 360)).toBe(8); + expect(getOptimalSegmentCount(dimensions, 1, 5, 360)).toBe(5); + }); + }); }); diff --git a/packages/grafana-ui/src/components/RadialGauge/utils.ts b/packages/grafana-ui/src/components/RadialGauge/utils.ts index 44f767d89b2..e26cf5eed2a 100644 --- a/packages/grafana-ui/src/components/RadialGauge/utils.ts +++ b/packages/grafana-ui/src/components/RadialGauge/utils.ts @@ -1,11 +1,38 @@ -import { FieldDisplay } from '@grafana/data'; +import { FieldDisplay, getDisplayProcessor } from '@grafana/data'; -export function getValueAngleForValue(fieldDisplay: FieldDisplay, startAngle: number, endAngle: number) { - const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; +import { RadialGaugeDimensions } from './types'; + +export function getFieldDisplayProcessor(displayValue: FieldDisplay) { + if (displayValue.view && displayValue.colIndex != null) { + const dp = displayValue.view.getFieldDisplayProcessor(displayValue.colIndex); + if (dp) { + return dp; + } + } + + return getDisplayProcessor(); +} + +export function getFieldConfigMinMax(fieldDisplay: FieldDisplay) { const min = fieldDisplay.field.min ?? 0; const max = fieldDisplay.field.max ?? 100; + return [min, max]; +} - let angle = ((fieldDisplay.display.numeric - min) / (max - min)) * angleRange; +export function getValuePercentageForValue(fieldDisplay: FieldDisplay, value = fieldDisplay.display.numeric) { + const [min, max] = getFieldConfigMinMax(fieldDisplay); + return (value - min) / (max - min); +} + +export function getValueAngleForValue( + fieldDisplay: FieldDisplay, + startAngle: number, + endAngle: number, + value = fieldDisplay.display.numeric +) { + const angleRange = (360 % (startAngle === 0 ? 1 : startAngle)) + endAngle; + + let angle = getValuePercentageForValue(fieldDisplay, value) * angleRange; if (angle > angleRange) { angle = angleRange; @@ -26,24 +53,19 @@ export function toRad(angle: number) { return ((angle - 90) * Math.PI) / 180; } -export interface GaugeDimensions { - margin: number; - radius: number; - centerX: number; - centerY: number; - barWidth: number; - endAngle?: number; - barIndex: number; - thresholdsBarRadius: number; - thresholdsBarWidth: number; - thresholdsBarSpacing: number; - showScaleLabels?: boolean; - scaleLabelsFontSize: number; - scaleLabelsSpacing: number; - scaleLabelsRadius: number; - gaugeBottomY: number; -} - +/** + * returns the calculated dimensions for the radial gauge + * @param width + * @param height + * @param endAngle + * @param glow + * @param roundedBars + * @param barWidthFactor + * @param barIndex + * @param thresholdBar + * @param showScaleLabels + * @returns {RadialGaugeDimensions} + */ export function calculateDimensions( width: number, height: number, @@ -54,7 +76,7 @@ export function calculateDimensions( barIndex: number, thresholdBar?: boolean, showScaleLabels?: boolean -): GaugeDimensions { +): RadialGaugeDimensions { const yMaxAngle = endAngle > 180 ? 180 : endAngle; let margin = 0; @@ -97,6 +119,7 @@ export function calculateDimensions( maxRadiusW -= labelsSize; maxRadiusH -= labelsSize; + // FIXME: needs coverage // For gauges the max label needs a bit more vertical space so that it does not get clipped if (maxRadiusIsLimitedByHeight && endAngle < 180) { const amount = outerRadius * 0.07; @@ -155,3 +178,105 @@ export function toCartesian(centerX: number, centerY: number, radius: number, an y: centerY + radius * Math.sin(radian), }; } + +export function drawRadialArcPath( + startAngle: number, + endAngle: number, + dimensions: RadialGaugeDimensions, + roundedBars?: boolean +): string { + const { radius, centerX, centerY, barWidth } = dimensions; + + // For some reason a 100% full arc cannot be rendered + if (endAngle >= 360) { + endAngle = 359.99; + } + + const startRadians = toRad(startAngle); + const endRadians = toRad(startAngle + endAngle); + + const largeArc = endAngle > 180 ? 1 : 0; + + const outerR = radius + barWidth / 2; + const innerR = Math.max(0, radius - barWidth / 2); + if (innerR <= 0) { + return ''; // cannot draw arc with 0 inner radius + } + + // get points for both an inner and outer arc. we draw + // the arc entirely with a path's fill instead of using stroke + // so that it can be used as a clip-path. + const ox1 = centerX + outerR * Math.cos(startRadians); + const oy1 = centerY + outerR * Math.sin(startRadians); + const ox2 = centerX + outerR * Math.cos(endRadians); + const oy2 = centerY + outerR * Math.sin(endRadians); + + const ix1 = centerX + innerR * Math.cos(startRadians); + const iy1 = centerY + innerR * Math.sin(startRadians); + const ix2 = centerX + innerR * Math.cos(endRadians); + const iy2 = centerY + innerR * Math.sin(endRadians); + + // calculate the cap width in case we're drawing rounded bars + const capR = barWidth / 2; + + const pathParts = [ + // start at outer start + 'M', + ox1, + oy1, + // outer arc from start to end (clockwise) + 'A', + outerR, + outerR, + 0, + largeArc, + 1, + ox2, + oy2, + ]; + + if (roundedBars) { + // rounded end cap: small arc connecting outer end to inner end + pathParts.push('A', capR, capR, 0, 0, 1, ix2, iy2); + } else { + // straight line to inner end (square butt) + pathParts.push('L', ix2, iy2); + } + + // inner arc from end back to start (counter-clockwise) + pathParts.push('A', innerR, innerR, 0, largeArc, 0, ix1, iy1); + + if (roundedBars) { + // rounded start cap: small arc connecting inner start back to outer start + pathParts.push('A', capR, capR, 0, 0, 1, ox1, oy1); + } else { + // straight line back to outer start (square butt) + pathParts.push('L', ox1, oy1); + } + + pathParts.push('Z'); + + return pathParts.join(' '); +} + +export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) { + // Max spacing is 8 degrees between segments + // Changing this constant could be considered a breaking change + const maxAngleBetweenSegments = Math.max(range / 1.5 / segmentCount, 2); + return segmentSpacing * maxAngleBetweenSegments; +} + +export function getOptimalSegmentCount( + dimensions: RadialGaugeDimensions, + segmentSpacing: number, + segmentCount: number, + range: number +) { + const angleBetweenSegments = getAngleBetweenSegments(segmentSpacing, segmentCount, range); + + const innerRadius = dimensions.radius - dimensions.barWidth / 2; + const circumference = Math.PI * innerRadius * 2 * (range / 360); + const maxSegments = Math.floor(circumference / (angleBetweenSegments + 3)); + + return Math.min(maxSegments, segmentCount); +} diff --git a/packages/grafana-ui/src/components/Select/ValueContainer.tsx b/packages/grafana-ui/src/components/Select/ValueContainer.tsx index 6eb8326ae5d..f39666c8c5f 100644 --- a/packages/grafana-ui/src/components/Select/ValueContainer.tsx +++ b/packages/grafana-ui/src/components/Select/ValueContainer.tsx @@ -76,4 +76,5 @@ class UnthemedValueContainer>> = + withTheme2(UnthemedValueContainer); diff --git a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts index 90ac3877c2a..1432c01e1da 100644 --- a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts +++ b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react'; -import { CSSObjectWithLabel } from 'react-select'; +import { StylesConfig } from 'react-select'; import { GrafanaTheme2 } from '@grafana/data'; -export default function resetSelectStyles(theme: GrafanaTheme2) { +export default function resetSelectStyles(theme: GrafanaTheme2): Partial { return { clearIndicator: () => ({}), container: () => ({}), @@ -13,7 +13,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2) { groupHeading: () => ({}), indicatorsContainer: () => ({}), indicatorSeparator: () => ({}), - input: function (originalStyles: CSSObjectWithLabel) { + input: function (originalStyles) { return { ...originalStyles, color: 'inherit', @@ -27,7 +27,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2) { loadingIndicator: () => ({}), loadingMessage: () => ({}), menu: () => ({}), - menuList: ({ maxHeight }: { maxHeight: number }) => ({ + menuList: ({ maxHeight }) => ({ maxHeight, }), multiValue: () => ({}), @@ -38,7 +38,7 @@ export default function resetSelectStyles(theme: GrafanaTheme2) { multiValueRemove: () => ({}), noOptionsMessage: () => ({}), option: () => ({}), - placeholder: (originalStyles: CSSObjectWithLabel) => ({ + placeholder: (originalStyles) => ({ ...originalStyles, color: theme.colors.text.secondary, }), @@ -47,11 +47,11 @@ export default function resetSelectStyles(theme: GrafanaTheme2) { }; } -export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | string | undefined) { +export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | string | undefined): Partial { return useMemo(() => { return { ...resetSelectStyles(theme), - menuPortal: (base: CSSObjectWithLabel) => { + menuPortal: (base) => { // 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 }: CSSObjectWithLabel) => { + menu: ({ top, bottom, position }) => { 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: CSSObjectWithLabel, state: any) => ({ + option: (provided, state) => ({ ...provided, opacity: state.isDisabled ? 0.5 : 1, }), diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx index 9ad678132e9..91f3d9945f3 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx @@ -27,4 +27,48 @@ describe('Sparkline', () => { render() ).not.toThrow(); }); + + it('should not throw an error if there is a single value', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1], + type: FieldType.number, + config: {}, + state: { + range: { min: 1, max: 1, delta: 0 }, + }, + }, + }; + expect(() => + render() + ).not.toThrow(); + }); + + it('should not throw an error if there are no values', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [], + type: FieldType.number, + config: {}, + state: {}, + }, + }; + expect(() => + render() + ).not.toThrow(); + }); }); diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index 8de8ed1b893..a9d3f039c42 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -1,203 +1,34 @@ -import { isEqual } from 'lodash'; -import { PureComponent } from 'react'; -import { AlignedData, Range } from 'uplot'; +import React, { memo } from 'react'; -import { - compareDataFrameStructures, - DataFrame, - Field, - FieldConfig, - FieldSparkline, - FieldType, - getFieldColorModeForField, - nullToValue, -} from '@grafana/data'; -import { - AxisPlacement, - GraphDrawStyle, - GraphFieldConfig, - VisibilityMode, - ScaleDirection, - ScaleOrientation, -} from '@grafana/schema'; +import { FieldConfig, FieldSparkline } from '@grafana/data'; +import { GraphFieldConfig } from '@grafana/schema'; import { Themeable2 } from '../../types/theme'; import { UPlotChart } from '../uPlot/Plot'; -import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { preparePlotData2, getStackingGroups } from '../uPlot/utils'; -import { getYRange, preparePlotFrame } from './utils'; +import { prepareSeries, prepareConfig } from './utils'; export interface SparklineProps extends Themeable2 { width: number; height: number; config?: FieldConfig; sparkline: FieldSparkline; + showHighlights?: boolean; } -interface State { - data: AlignedData; - alignedDataFrame: DataFrame; - configBuilder: UPlotConfigBuilder; -} +export const Sparkline: React.FC = memo((props) => { + const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props; -const defaultConfig: GraphFieldConfig = { - drawStyle: GraphDrawStyle.Line, - showPoints: VisibilityMode.Auto, - axisPlacement: AxisPlacement.Hidden, - pointSize: 2, -}; - -/** @internal */ -export class Sparkline extends PureComponent { - constructor(props: SparklineProps) { - super(props); - - const alignedDataFrame = preparePlotFrame(props.sparkline, props.config); - - this.state = { - data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)), - alignedDataFrame, - configBuilder: this.prepareConfig(alignedDataFrame), - }; + const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights); + if (warning) { + return null; } - static getDerivedStateFromProps(props: SparklineProps, state: State) { - const _frame = preparePlotFrame(props.sparkline, props.config); - const frame = nullToValue(_frame); - if (!frame) { - return { ...state }; - } + const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)); + const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme, showHighlights); - return { - ...state, - data: preparePlotData2(frame, getStackingGroups(frame)), - alignedDataFrame: frame, - }; - } + return ; +}); - componentDidUpdate(prevProps: SparklineProps, prevState: State) { - const { alignedDataFrame } = this.state; - - if (!alignedDataFrame) { - return; - } - - let rebuildConfig = false; - - if (prevProps.sparkline !== this.props.sparkline) { - const isStructureChanged = !compareDataFrameStructures(this.state.alignedDataFrame, prevState.alignedDataFrame); - const isRangeChanged = !isEqual( - alignedDataFrame.fields[1].state?.range, - prevState.alignedDataFrame.fields[1].state?.range - ); - rebuildConfig = isStructureChanged || isRangeChanged; - } else { - rebuildConfig = !isEqual(prevProps.config, this.props.config); - } - - if (rebuildConfig) { - this.setState({ configBuilder: this.prepareConfig(alignedDataFrame) }); - } - } - - getYRange(field: Field): Range.MinMax { - return getYRange(field, this.state.alignedDataFrame); - } - - prepareConfig(data: DataFrame) { - const { theme } = this.props; - const builder = new UPlotConfigBuilder(); - - builder.setCursor({ - show: false, - x: false, // no crosshairs - y: false, - }); - - // X is the first field in the alligned frame - const xField = data.fields[0]; - builder.addScale({ - scaleKey: 'x', - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, - isTime: false, //xField.type === FieldType.time, - range: () => { - const { sparkline } = this.props; - if (sparkline.x) { - if (sparkline.timeRange && sparkline.x.type === FieldType.time) { - return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()]; - } - const vals = sparkline.x.values; - return [vals[0], vals[vals.length - 1]]; - } - return [0, sparkline.y.values.length - 1]; - }, - }); - - builder.addAxis({ - scaleKey: 'x', - theme, - placement: AxisPlacement.Hidden, - }); - - for (let i = 0; i < data.fields.length; i++) { - const field = data.fields[i]; - const config: FieldConfig = field.config; - const customConfig: GraphFieldConfig = { - ...defaultConfig, - ...config.custom, - }; - - if (field === xField || field.type !== FieldType.number) { - continue; - } - - const scaleKey = config.unit || '__fixed'; - builder.addScale({ - scaleKey, - orientation: ScaleOrientation.Vertical, - direction: ScaleDirection.Up, - range: () => this.getYRange(field), - }); - - builder.addAxis({ - scaleKey, - theme, - placement: AxisPlacement.Hidden, - }); - - const colorMode = getFieldColorModeForField(field); - const seriesColor = colorMode.getCalculator(field, theme)(0, 0); - const pointsMode = - customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; - - builder.addSeries({ - pxAlign: false, - scaleKey, - theme, - colorMode, - thresholds: config.thresholds, - drawStyle: customConfig.drawStyle!, - lineColor: customConfig.lineColor ?? seriesColor, - lineWidth: customConfig.lineWidth, - lineInterpolation: customConfig.lineInterpolation, - showPoints: pointsMode, - pointSize: customConfig.pointSize, - fillOpacity: customConfig.fillOpacity, - fillColor: customConfig.fillColor, - lineStyle: customConfig.lineStyle, - gradientMode: customConfig.gradientMode, - spanNulls: customConfig.spanNulls, - }); - } - - return builder; - } - - render() { - const { data, configBuilder } = this.state; - const { width, height } = this.props; - return ; - } -} +Sparkline.displayName = 'Sparkline'; diff --git a/packages/grafana-ui/src/components/Sparkline/utils.test.ts b/packages/grafana-ui/src/components/Sparkline/utils.test.ts index 9eb914af438..0ec65515e0c 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.test.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.test.ts @@ -1,6 +1,6 @@ -import { Field, FieldSparkline, FieldType } from '@grafana/data'; +import { createTheme, Field, FieldSparkline, FieldType, toDataFrame } from '@grafana/data'; -import { getYRange, preparePlotFrame } from './utils'; +import { getYRange, prepareConfig, preparePlotFrame } from './utils'; describe('Prepare Sparkline plot frame', () => { it('should return sorted array if x-axis numeric', () => { @@ -114,6 +114,20 @@ describe('Get y range', () => { config: {}, state: { range: { min: -2, max: -2, delta: 0 } }, }; + const decimalsCloseYField: Field = { + name: 'y', + values: [2, 1.999999999999999, 2.000000000000001, 2, 2], + type: FieldType.number, + config: {}, + state: { range: { min: 1.9999999999999999999, max: 2.000000000000000001, delta: 0 } }, + }; + const decimalsNotCloseYField: Field = { + name: 'y', + values: [2, 0.0094, 0.0053, 0.0078, 0.0061], + type: FieldType.number, + config: {}, + state: { range: { min: 0.0053, max: 0.0094, delta: 0.0041 } }, + }; const xField: Field = { name: 'x', values: [1000, 2000, 3000, 4000, 5000], @@ -154,12 +168,12 @@ describe('Get y range', () => { { description: 'straight line', field: straightLineYField, - expected: [0, 4], + expected: [2, 4], }, { description: 'straight line, negative values', field: straightLineNegYField, - expected: [-4, 0], + expected: [-4, -2], }, { description: 'straight line with config min and max', @@ -171,9 +185,150 @@ describe('Get y range', () => { field: { ...straightLineYField, config: { noValue: '0' } }, expected: [0, 2], }, + { + description: 'long decimals which are nearly equal and result in a functional delta of 0', + field: decimalsCloseYField, + expected: [2, 4], + }, + { + description: 'decimal values which are not close to equal should not be rounded out', + field: decimalsNotCloseYField, + expected: [0.0053, 0.0094], + }, ])(`should return correct range for $description`, ({ field, expected }) => { - const actual = getYRange(field, getAlignedFrame(field)); + const actual = getYRange(getAlignedFrame(field)); expect(actual).toEqual(expected); expect(actual[0]).toBeLessThan(actual[1]!); }); }); + +describe('prepareConfig', () => { + it('should not throw an error if there are multiple values', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1, 2, 3, 4, 5], + type: FieldType.number, + config: {}, + }, + }; + + const dataFrame = toDataFrame({ + fields: [sparkline.x, sparkline.y], + }); + + const config = prepareConfig(sparkline, dataFrame, createTheme()); + expect(config.series.length).toBe(1); + }); + + it('should not throw an error if there is a single value', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1], + type: FieldType.number, + config: {}, + }, + }; + + const dataFrame = toDataFrame({ + fields: [sparkline.x, sparkline.y], + }); + + const config = prepareConfig(sparkline, dataFrame, createTheme()); + expect(config.series.length).toBe(1); + }); + + it('should not throw an error if there are no values', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [], + type: FieldType.number, + config: {}, + }, + }; + + const dataFrame = toDataFrame({ + fields: [sparkline.x, sparkline.y], + }); + + const config = prepareConfig(sparkline, dataFrame, createTheme()); + expect(config.series.length).toBe(1); + }); + + it('should set up highlight series if showHighlights is true and highlightIdx exists', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1, 2, 3, 4, 5], + type: FieldType.number, + config: {}, + }, + highlightIndex: 2, + }; + + const dataFrame = toDataFrame({ + fields: [sparkline.x, sparkline.y], + }); + + const config = prepareConfig(sparkline, dataFrame, createTheme(), true); + expect(config.series.length).toBe(1); + expect(config.series[0].getConfig().points).toEqual( + expect.objectContaining({ + show: true, + filter: [2], + }) + ); + }); + + it('should not set up highlight series if showHighlights is false even if highlightIdx exists', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1, 2, 3, 4, 5], + type: FieldType.number, + config: {}, + }, + highlightIndex: 2, + }; + + const dataFrame = toDataFrame({ + fields: [sparkline.x, sparkline.y], + }); + + const config = prepareConfig(sparkline, dataFrame, createTheme(), false); + expect(config.series.length).toBe(1); + expect(config.series[0].getConfig().points?.show).not.toBe(true); + }); +}); diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index 5c2afd8a1cb..c1402c4da2d 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -1,16 +1,32 @@ import { Range } from 'uplot'; import { + applyNullInsertThreshold, + // colorManipulator, DataFrame, FieldConfig, FieldSparkline, FieldType, + getFieldColorModeForField, + GrafanaTheme2, + guessDecimals, isLikelyAscendingVector, + nullToValue, + roundDecimals, sortDataFrame, - applyNullInsertThreshold, - Field, } from '@grafana/data'; -import { GraphFieldConfig } from '@grafana/schema'; +import { t } from '@grafana/i18n'; +import { + AxisPlacement, + GraphDrawStyle, + GraphFieldConfig, + VisibilityMode, + ScaleDirection, + ScaleOrientation, + // FieldColorModeId, +} from '@grafana/schema'; + +import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; /** @internal * Given a sparkline config returns a DataFrame ready to be turned into Plot data set @@ -55,33 +71,200 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig /** * apply configuration defaults and ensure that the range is never two equal values. */ -export function getYRange(field: Field, alignedFrame: DataFrame): Range.MinMax { - let { min, max } = alignedFrame.fields[1].state?.range!; +export function getYRange(alignedFrame: DataFrame): Range.MinMax { + const field = alignedFrame.fields[1]; + let { min, max } = field.state?.range!; - // enure that the min/max from the field config are respected - min = Math.max(min!, field.config.min ?? -Infinity); - max = Math.min(max!, field.config.max ?? Infinity); + // enure that the min/max from the field config are respected. + min = Math.min(min!, field.config.min ?? Infinity); + max = Math.max(max!, field.config.max ?? -Infinity); // if noValue is set, ensure that it is included in the range as well - const noValue = +alignedFrame.fields[1].config?.noValue!; + const noValue = +field.config?.noValue!; if (!Number.isNaN(noValue)) { min = Math.min(min, noValue); max = Math.max(max, noValue); } - // if min and max are equal after all of that, create a range - // that allows the sparkline to be visible in the center of the viz - if (min === max) { - if (min === 0) { - max = 100; - } else if (min < 0) { - max = 0; - min *= 2; - } else { - min = 0; - max *= 2; - } + const decimals = field.config.decimals ?? Math.max(guessDecimals(min), guessDecimals(max)); + + // call roundDecimals to mirror what is going to eventually happen in uplot + let roundedMin = roundDecimals(min, decimals); + let roundedMax = roundDecimals(max, decimals); + + // if the rounded min and max are different, + // we can return the real min and max. + if (roundedMin !== roundedMax) { + return [min, max]; } - return [min, max]; + // we are forced to tweak the min and max since they + // will be treated as equal after rounding by uPlot. + if (roundedMin === 0) { + // both are zero + roundedMax = 1; + } else if (roundedMin < 0) { + // both are negative + roundedMin *= 2; + } else { + // both are positive + roundedMax *= 2; + } + + return [roundedMin, roundedMax]; } + +const HIGHLIGHT_IDX_POINT_SIZE = 6; + +const defaultConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + showPoints: VisibilityMode.Auto, + axisPlacement: AxisPlacement.Hidden, + pointSize: 2, +}; + +export const prepareSeries = ( + sparkline: FieldSparkline, + _theme: GrafanaTheme2, + fieldConfig?: FieldConfig, + _showHighlights?: boolean +): { frame: DataFrame; warning?: string } => { + const frame = nullToValue(preparePlotFrame(sparkline, fieldConfig)); + if (frame.fields.some((f) => f.values.length <= 1)) { + return { + warning: t( + 'grafana-ui.components.sparkline.warning.too-few-values', + 'Sparkline requires at least two values to render.' + ), + frame, + }; + } + // TODO:rgb(24, 24, 24) will address this. + // if (showHighlights && typeof sparkline.highlightLine === 'number') { + // const highlightY = sparkline.highlightLine; + // const colorMode = getFieldColorModeForField(sparkline.y); + // const seriesColor = colorMode.getCalculator(sparkline.y, theme)(highlightY, 0); + // frame.fields.push({ + // name: 'highlightLine', + // type: FieldType.number, + // values: new Array(frame.length).fill(highlightY), + // config: { + // color: { + // mode: FieldColorModeId.Fixed, + // fixedColor: colorManipulator.lighten(seriesColor, 0.5), + // }, + // custom: { + // lineStyle: { + // fill: 'dash', + // dash: [5, 2], + // }, + // }, + // }, + // state: {}, + // }); + // } + return { frame }; +}; + +export const prepareConfig = ( + sparkline: FieldSparkline, + dataFrame: DataFrame, + theme: GrafanaTheme2, + showHighlights?: boolean +): UPlotConfigBuilder => { + const builder = new UPlotConfigBuilder(); + const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2; + + builder.setCursor({ + show: false, + x: false, // no crosshairs + y: false, + }); + + // X is the first field in the aligned frame + const xField = dataFrame.fields[0]; + builder.addScale({ + scaleKey: 'x', + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + isTime: false, // xField.type === FieldType.time, + range: () => { + if (sparkline.x) { + if (sparkline.timeRange && sparkline.x.type === FieldType.time) { + return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()]; + } + const vals = sparkline.x.values; + return [vals[0], vals[vals.length - 1]]; + } + return [0, sparkline.y.values.length - 1]; + }, + }); + + builder.addAxis({ + scaleKey: 'x', + theme, + placement: AxisPlacement.Hidden, + }); + + for (let i = 0; i < dataFrame.fields.length; i++) { + const field = dataFrame.fields[i]; + const config: FieldConfig = field.config; + const customConfig: GraphFieldConfig = { + ...defaultConfig, + ...config.custom, + }; + + if (field === xField || field.type !== FieldType.number) { + continue; + } + + const scaleKey = config.unit || '__fixed'; + builder.addScale({ + scaleKey, + orientation: ScaleOrientation.Vertical, + direction: ScaleDirection.Up, + range: () => getYRange(dataFrame), + }); + + builder.addAxis({ + scaleKey, + theme, + placement: AxisPlacement.Hidden, + }); + + const colorMode = getFieldColorModeForField(field); + const seriesColor = colorMode.getCalculator(field, theme)(0, 0); + + const hasHighlightIndex = showHighlights && typeof sparkline.highlightIndex === 'number'; + if (hasHighlightIndex) { + builder.setPadding([rangePad, rangePad, rangePad, rangePad]); + } + + const pointsMode = + customConfig.drawStyle === GraphDrawStyle.Points || hasHighlightIndex + ? VisibilityMode.Always + : customConfig.showPoints; + + builder.addSeries({ + pxAlign: false, + scaleKey, + theme, + colorMode, + thresholds: config.thresholds, + drawStyle: customConfig.drawStyle!, + lineColor: customConfig.lineColor ?? seriesColor, + lineWidth: customConfig.lineWidth, + lineInterpolation: customConfig.lineInterpolation, + showPoints: pointsMode, + pointSize: hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : customConfig.pointSize, + pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined, + fillOpacity: customConfig.fillOpacity, + fillColor: customConfig.fillColor, + lineStyle: customConfig.lineStyle, + gradientMode: customConfig.gradientMode, + spanNulls: customConfig.spanNulls, + }); + } + + return builder; +}; diff --git a/packages/grafana-ui/src/components/Table/CellActions.tsx b/packages/grafana-ui/src/components/Table/CellActions.tsx index e2fe8a658ba..f21be659c4f 100644 --- a/packages/grafana-ui/src/components/Table/CellActions.tsx +++ b/packages/grafana-ui/src/components/Table/CellActions.tsx @@ -1,3 +1,4 @@ +import { isPlainObject } from 'lodash'; import { useCallback } from 'react'; import * as React from 'react'; @@ -63,7 +64,18 @@ export function CellActions({ tooltip={t('grafana-ui.table.cell-inspect', 'Inspect value')} onClick={() => { if (setInspectCell) { - setInspectCell({ value: cell.value, mode: previewMode }); + let mode = TableCellInspectorMode.text; + let inspectValue = cell.value; + try { + const parsed = typeof inspectValue === 'string' ? JSON.parse(inspectValue) : inspectValue; + if (Array.isArray(parsed) || isPlainObject(parsed)) { + inspectValue = JSON.stringify(parsed, null, 2); + mode = TableCellInspectorMode.code; + } + } catch { + // do nothing + } + setInspectCell({ value: inspectValue, mode }); } }} {...commonButtonProps} diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index b6c97df317e..e3d5e4b56cc 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -263,7 +263,16 @@ export const Footer: StoryFn = (args) => { ); }; -export const Pagination: StoryFn = (args) => ; +export const Pagination: StoryFn = (args) => { + const theme = useTheme2(); + const data = buildData(theme, {}); + + return ( + + + + ); +}; Pagination.args = { enablePagination: true, }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx index 56e38b9cd60..ead8014e550 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx @@ -451,6 +451,19 @@ describe('TableNG', () => { expect(screen.getByText('A1')).toBeInTheDocument(); expect(screen.getByText('1')).toBeInTheDocument(); }); + + it('shows full column name in title attribute for truncated headers', () => { + const { container } = render( + + ); + + const headers = container.querySelectorAll('[role="columnheader"]'); + const firstHeaderSpan = headers[0].querySelector('span'); + const secondHeaderSpan = headers[1].querySelector('span'); + + expect(firstHeaderSpan).toHaveAttribute('title', 'Column A'); + expect(secondHeaderSpan).toHaveAttribute('title', 'Column B'); + }); }); describe('Footer options', () => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 798ca4e1ae0..f17a62b92cf 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -105,6 +105,7 @@ export function TableNG(props: TableNGProps) { const { cellHeight, data, + disableKeyboardEvents, disableSanitizeHtml, enablePagination = false, enableSharedCrosshair = false, @@ -153,8 +154,18 @@ export function TableNG(props: TableNGProps) { const resizeHandler = useColumnResize(onColumnResize); - const rows = useMemo(() => frameToRecords(data), [data]); const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]); + const nestedFramesFieldName = useMemo(() => { + if (!hasNestedFrames) { + return; + } + const firstNestedField = data.fields.find((f) => f.type === FieldType.nestedFrames); + if (!firstNestedField) { + return; + } + return getDisplayName(firstNestedField); + }, [data, hasNestedFrames]); + const rows = useMemo(() => frameToRecords(data, nestedFramesFieldName), [data, nestedFramesFieldName]); const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []); const { @@ -373,7 +384,11 @@ export function TableNG(props: TableNGProps) { return null; } - const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns); + const expandedRecords = applySort( + frameToRecords(nestedData, nestedFramesFieldName), + nestedData.fields, + sortColumns + ); if (!expandedRecords.length) { return (
@@ -397,7 +412,7 @@ export function TableNG(props: TableNGProps) { width: COLUMN.EXPANDER_WIDTH, minWidth: COLUMN.EXPANDER_WIDTH, }), - [commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles] + [commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles, nestedFramesFieldName] ); const fromFields = useCallback( @@ -819,9 +834,9 @@ export function TableNG(props: TableNGProps) { } }} onCellKeyDown={ - hasNestedFrames + hasNestedFrames || disableKeyboardEvents ? (_, event) => { - if (event.isDefaultPrevented()) { + if (disableKeyboardEvents || event.isDefaultPrevented()) { // skip parent grid keyboard navigation if nested grid handled it event.preventGridDefault(); } diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx index 8ccc73a98b9..fc59c08aa3c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx @@ -55,7 +55,9 @@ const HeaderCell: React.FC = ({ {showTypeIcons && ( )} - {getDisplayName(field)} + + {displayName} + {direction && ( +
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index b828dfddbb6..ddfaf189f34 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -79,7 +79,6 @@ export interface TableRow { // Nested table properties data?: DataFrame; - __nestedFrames?: DataFrame[]; __expanded?: boolean; // For row expansion state // Generic typing for column values @@ -138,6 +137,8 @@ export interface BaseTableProps { enableVirtualization?: boolean; // for MarkdownCell, this flag disables sanitization of HTML content. Configured via config.ini. disableSanitizeHtml?: boolean; + // if true, disables all keyboard events in the table. this is used when previewing a table (i.e. suggestions) + disableKeyboardEvents?: boolean; } /* ---------------------------- Table cell props ---------------------------- */ @@ -260,7 +261,7 @@ export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOpti export type Comparator = (a: TableCellValue, b: TableCellValue) => number; // Type for converting a DataFrame into an array of TableRows -export type FrameToRowsConverter = (frame: DataFrame) => TableRow[]; +export type FrameToRowsConverter = (frame: DataFrame, nestedFramesFieldName?: string) => TableRow[]; // Type for mapping column names to their field types export type ColumnTypes = Record; diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 0226f8b6463..b960d8c08c5 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -675,10 +675,12 @@ export function applySort( /** * @internal */ -export const frameToRecords = (frame: DataFrame): TableRow[] => { +export const frameToRecords = (frame: DataFrame, nestedFramesFieldName?: string): TableRow[] => { const fnBody = ` const rows = Array(frame.length); const values = frame.fields.map(f => f.values); + const hasNestedFrames = '${nestedFramesFieldName ?? ''}'.length > 0; + let rowCount = 0; for (let i = 0; i < frame.length; i++) { rows[rowCount] = { @@ -686,11 +688,14 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => { __index: i, ${frame.fields.map((field, fieldIdx) => `${JSON.stringify(getDisplayName(field))}: values[${fieldIdx}][i]`).join(',')} }; - rowCount += 1; - if (rows[rowCount-1]['__nestedFrames']){ - const childFrame = rows[rowCount-1]['__nestedFrames']; - rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]} - rowCount += 1; + rowCount++; + + if (hasNestedFrames) { + const childFrame = rows[rowCount-1][${JSON.stringify(nestedFramesFieldName)}]; + if (childFrame){ + rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]} + rowCount++; + } } } return rows; @@ -698,8 +703,9 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => { // Creates a function that converts a DataFrame into an array of TableRows // Uses new Function() for performance as it's faster than creating rows using loops - const convert = new Function('frame', fnBody) as FrameToRowsConverter; - return convert(frame); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const convert = new Function('frame', 'nestedFramesFieldName', fnBody) as FrameToRowsConverter; + return convert(frame, nestedFramesFieldName); }; /* ----------------------------- Data grid comparator ---------------------------- */ diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index c1f99b0db93..2193103a90f 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -206,7 +206,7 @@ const getStyles = (theme: GrafanaTheme2) => { background: 'transparent', border: `1px solid transparent`, - '&:hover, &:focus': { + '&:hover': { color: theme.colors.text.primary, background: theme.colors.action.hover, }, diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx index cba6f984501..b9af4d23fc4 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx @@ -16,10 +16,6 @@ const meta: Meta = { containerWidth: '100%', seriesCount: 5, }, - parameters: { - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, - }, argTypes: { containerWidth: { control: { diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx new file mode 100644 index 00000000000..131133bcdfb --- /dev/null +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx @@ -0,0 +1,78 @@ +import { render, screen } from '@testing-library/react'; + +import { VizLegendTable } from './VizLegendTable'; +import { VizLegendItem } from './types'; + +describe('VizLegendTable', () => { + const mockItems: VizLegendItem[] = [ + { label: 'Series 1', color: 'red', yAxis: 1 }, + { label: 'Series 2', color: 'blue', yAxis: 1 }, + { label: 'Series 3', color: 'green', yAxis: 1 }, + ]; + + it('renders without crashing', () => { + const { container } = render(); + expect(container.querySelector('table')).toBeInTheDocument(); + }); + + it('renders all items', () => { + render(); + expect(screen.getByText('Series 1')).toBeInTheDocument(); + expect(screen.getByText('Series 2')).toBeInTheDocument(); + expect(screen.getByText('Series 3')).toBeInTheDocument(); + }); + + it('renders table headers when items have display values', () => { + const itemsWithStats: VizLegendItem[] = [ + { + label: 'Series 1', + color: 'red', + yAxis: 1, + getDisplayValues: () => [ + { numeric: 100, text: '100', title: 'Max' }, + { numeric: 50, text: '50', title: 'Min' }, + ], + }, + ]; + render(); + expect(screen.getByText('Max')).toBeInTheDocument(); + expect(screen.getByText('Min')).toBeInTheDocument(); + }); + + it('renders sort icon when sorted', () => { + const { container } = render( + + ); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); + + it('calls onToggleSort when header is clicked', () => { + const onToggleSort = jest.fn(); + render(); + const header = screen.getByText('Name'); + header.click(); + expect(onToggleSort).toHaveBeenCalledWith('Name'); + }); + + it('does not call onToggleSort when not sortable', () => { + const onToggleSort = jest.fn(); + render(); + const header = screen.getByText('Name'); + header.click(); + expect(onToggleSort).not.toHaveBeenCalled(); + }); + + it('renders with long labels', () => { + const itemsWithLongLabels: VizLegendItem[] = [ + { + label: 'This is a very long series name that should be scrollable within its table cell', + color: 'red', + yAxis: 1, + }, + ]; + render(); + expect( + screen.getByText('This is a very long series name that should be scrollable within its table cell') + ).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx index b0f578fae79..0c2859453eb 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx @@ -29,11 +29,9 @@ export const VizLegendTable = ({ isSortable, }: VizLegendTableProps): JSX.Element => { const styles = useStyles2(getStyles); - const header: Record = {}; - - if (isSortable) { - header[nameSortKey] = ''; - } + const header: Record = { + [nameSortKey]: '', + }; for (const item of items) { if (item.getDisplayValues) { @@ -90,16 +88,18 @@ export const VizLegendTable = ({
- {!isSortable && } {Object.keys(header).map((columnTitle) => ( - {item.getDisplayValues && @@ -128,6 +130,27 @@ const getStyles = (theme: GrafanaTheme2) => { background: rowHoverBg, }, }), + labelCell: css({ + label: 'LegendLabelCell', + maxWidth: 0, + width: '100%', + }), + labelCellInner: css({ + label: 'LegendLabelCellInner', + display: 'block', + flex: 1, + minWidth: 0, + overflowX: 'auto', + overflowY: 'hidden', + paddingRight: theme.spacing(3), + scrollbarWidth: 'none', + msOverflowStyle: 'none', + maskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, + WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, + '&::-webkit-scrollbar': { + display: 'none', + }, + }), label: css({ label: 'LegendLabel', whiteSpace: 'nowrap', @@ -135,9 +158,6 @@ const getStyles = (theme: GrafanaTheme2) => { border: 'none', fontSize: 'inherit', padding: 0, - maxWidth: '600px', - textOverflow: 'ellipsis', - overflow: 'hidden', userSelect: 'text', }), labelDisabled: css({ diff --git a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx index a1fb6c6a1c3..735e198f0bc 100644 --- a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx +++ b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx @@ -167,7 +167,8 @@ export class VizRepeater extends PureComponent 0 { + extraArgs := make([]any, 0, len(e.Extra)*2) + + for k, v := range e.Extra { + extraArgs = append(extraArgs, "extra_"+k, v) + } + + args = append(args, extraArgs...) + } + + return args +} + +type EventOutcome string + +const ( + EventOutcomeUnknown EventOutcome = "unknown" + EventOutcomeSuccess EventOutcome = "success" + EventOutcomeFailureUnauthorized EventOutcome = "failure_unauthorized" + EventOutcomeFailureNotFound EventOutcome = "failure_not_found" + EventOutcomeFailureGeneric EventOutcome = "failure_generic" +) diff --git a/pkg/apiserver/auditing/event_test.go b/pkg/apiserver/auditing/event_test.go new file mode 100644 index 00000000000..3267936b02a --- /dev/null +++ b/pkg/apiserver/auditing/event_test.go @@ -0,0 +1,64 @@ +package auditing_test + +import ( + "encoding/json" + "strconv" + "strings" + "testing" + "time" + + "github.com/grafana/grafana/pkg/apiserver/auditing" + "github.com/stretchr/testify/require" +) + +func TestEvent_MarshalJSON(t *testing.T) { + t.Parallel() + + t.Run("marshals the event", func(t *testing.T) { + t.Parallel() + + now := time.Now() + + event := auditing.Event{ + ObservedAt: now, + Extra: map[string]string{"k1": "v1", "k2": "v2"}, + } + + data, err := json.Marshal(event) + require.NoError(t, err) + + var result map[string]any + require.NoError(t, json.Unmarshal(data, &result)) + + require.Equal(t, event.Time().UTC().Format(time.RFC3339Nano), result["observedAt"]) + require.NotNil(t, result["extra"]) + require.Len(t, result["extra"], 2) + }) +} + +func TestEvent_KVPairs(t *testing.T) { + t.Parallel() + + t.Run("records extra fields", func(t *testing.T) { + t.Parallel() + + extraFields := 2 + extra := make(map[string]string, 0) + for i := 0; i < extraFields; i++ { + extra[strconv.Itoa(i)] = "value" + } + + event := auditing.Event{Extra: extra} + + kvPairs := event.KVPairs() + + extraCount := 0 + for i := 0; i < len(kvPairs); i += 2 { + if strings.HasPrefix(kvPairs[i].(string), "extra_") { + extraCount++ + } + } + + require.Equal(t, extraCount, extraFields) + }) +} diff --git a/pkg/apiserver/auditing/logger.go b/pkg/apiserver/auditing/logger.go new file mode 100644 index 00000000000..8e60d463255 --- /dev/null +++ b/pkg/apiserver/auditing/logger.go @@ -0,0 +1,55 @@ +package auditing + +import ( + "context" + "encoding/json" + "time" +) + +// Sinkable is a log entry abstraction that can be sent to an audit log sink through the different implementing methods. +type Sinkable interface { + json.Marshaler + KVPairs() []any + Time() time.Time +} + +// Logger specifies the contract for a specific audit logger. +type Logger interface { + Log(entry Sinkable) error + Close() error + Type() string +} + +// Implementation inspired by https://github.com/grafana/grafana-app-sdk/blob/main/logging/logger.go +type loggerContextKey struct{} + +var ( + // DefaultLogger is the default Logger if one hasn't been provided in the context. + // You may use this to add arbitrary audit logging outside of an API request lifecycle. + DefaultLogger Logger = &NoopLogger{} + + contextKey = loggerContextKey{} +) + +// FromContext returns the Logger set in the context with Context(), or the DefaultLogger if no Logger is set in the context. +// If DefaultLogger is nil, it returns a *NoopLogger so that the return is always valid to call methods on without nil-checking. +// You may use this to add arbitrary audit logging outside of an API request lifecycle. +func FromContext(ctx context.Context) Logger { + if l := ctx.Value(contextKey); l != nil { + if logger, ok := l.(Logger); ok { + return logger + } + } + + if DefaultLogger != nil { + return DefaultLogger + } + + return &NoopLogger{} +} + +// Context returns a new context built from the provided context with the provided logger in it. +// The Logger added with Context() can be retrieved with FromContext() +func Context(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, contextKey, logger) +} diff --git a/pkg/apiserver/auditing/noop.go b/pkg/apiserver/auditing/noop.go new file mode 100644 index 00000000000..c36c3577a09 --- /dev/null +++ b/pkg/apiserver/auditing/noop.go @@ -0,0 +1,47 @@ +package auditing + +import ( + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +// NoopBackend is a no-op implementation of audit.Backend +type NoopBackend struct{} + +func ProvideNoopBackend() audit.Backend { return &NoopBackend{} } + +func (NoopBackend) ProcessEvents(...*auditinternal.Event) bool { return false } + +func (NoopBackend) Run(<-chan struct{}) error { return nil } + +func (NoopBackend) Shutdown() {} + +func (NoopBackend) String() string { return "" } + +// NoopPolicyRuleProvider is a no-op implementation of PolicyRuleProvider +type NoopPolicyRuleProvider struct{} + +func ProvideNoopPolicyRuleProvider() PolicyRuleProvider { return &NoopPolicyRuleProvider{} } + +func (NoopPolicyRuleProvider) PolicyRuleProvider(PolicyRuleEvaluators) audit.PolicyRuleEvaluator { + return NoopPolicyRuleEvaluator{} +} + +// NoopPolicyRuleEvaluator is a no-op implementation of audit.PolicyRuleEvaluator +type NoopPolicyRuleEvaluator struct{} + +func (NoopPolicyRuleEvaluator) EvaluatePolicyRule(authorizer.Attributes) audit.RequestAuditConfig { + return audit.RequestAuditConfig{Level: auditinternal.LevelNone} +} + +// NoopLogger is a no-op implementation of Logger +type NoopLogger struct{} + +func ProvideNoopLogger() Logger { return &NoopLogger{} } + +func (NoopLogger) Type() string { return "noop" } + +func (NoopLogger) Log(Sinkable) error { return nil } + +func (NoopLogger) Close() error { return nil } diff --git a/pkg/apiserver/auditing/policy.go b/pkg/apiserver/auditing/policy.go new file mode 100644 index 00000000000..ed053ca205d --- /dev/null +++ b/pkg/apiserver/auditing/policy.go @@ -0,0 +1,68 @@ +package auditing + +import ( + "slices" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "k8s.io/apimachinery/pkg/runtime/schema" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +// PolicyRuleEvaluators is a map of API group+version to audit.PolicyRuleEvaluator +type PolicyRuleEvaluators = map[schema.GroupVersion]audit.PolicyRuleEvaluator + +type PolicyRuleProvider interface { + PolicyRuleProvider(evaluators PolicyRuleEvaluators) audit.PolicyRuleEvaluator +} + +// PolicyRuleEvaluator alias for easier imports. +type PolicyRuleEvaluator = audit.PolicyRuleEvaluator + +// DefaultGrafanaPolicyRuleEvaluator provides a sane default configuration for audit logging for API group+versions. +type defaultGrafanaPolicyRuleEvaluator struct{} + +var _ PolicyRuleEvaluator = &defaultGrafanaPolicyRuleEvaluator{} + +func NewDefaultGrafanaPolicyRuleEvaluator() audit.PolicyRuleEvaluator { + return defaultGrafanaPolicyRuleEvaluator{} +} + +func (defaultGrafanaPolicyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) audit.RequestAuditConfig { + // Skip non-resource and watch requests otherwise it is too noisy. + if !attrs.IsResourceRequest() || attrs.GetVerb() == utils.VerbWatch { + return audit.RequestAuditConfig{ + Level: auditinternal.LevelNone, + } + } + + // Skip auditing if the user is part of the privileged group. + // The loopback client uses this group, so requests initiated in `/api/` would be duplicated. + if u := attrs.GetUser(); u != nil && slices.Contains(u.GetGroups(), user.SystemPrivilegedGroup) { + return audit.RequestAuditConfig{ + Level: auditinternal.LevelNone, + } + } + + // Logging the response object allows us to get the resource name for create requests. + level := auditinternal.LevelMetadata + if attrs.GetVerb() == utils.VerbCreate { + level = auditinternal.LevelRequestResponse + } + + return audit.RequestAuditConfig{ + Level: level, + + // Only log on StageResponseComplete, to avoid noisy logs. + OmitStages: []auditinternal.Stage{ + auditinternal.StageRequestReceived, + auditinternal.StageResponseStarted, + auditinternal.StagePanic, + }, + + // Setting it to true causes extra copying/unmarshalling. + OmitManagedFields: false, + } +} diff --git a/pkg/apiserver/auditing/policy_test.go b/pkg/apiserver/auditing/policy_test.go new file mode 100644 index 00000000000..ccabaa5e6e2 --- /dev/null +++ b/pkg/apiserver/auditing/policy_test.go @@ -0,0 +1,89 @@ +package auditing_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apiserver/auditing" + "github.com/stretchr/testify/require" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestDefaultGrafanaPolicyRuleEvaluator(t *testing.T) { + t.Parallel() + + evaluator := auditing.NewDefaultGrafanaPolicyRuleEvaluator() + require.NotNil(t, evaluator) + + t.Run("returns audit level none for non-resource requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: false, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("returns audit level none for watch requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbWatch, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("returns audit level none for requests from privileged group", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbCreate, + User: &user.DefaultInfo{ + Groups: []string{"test-group", user.SystemPrivilegedGroup}, + }, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("return audit level request+response for create requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbCreate, + User: &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"test-group"}, + }, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelRequestResponse, config.Level) + }) + + t.Run("return audit level metadata for other resource requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbGet, + User: &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"test-group"}, + }, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelMetadata, config.Level) + }) +} diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index feb659b9a0a..89edb050ed8 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,18 +5,18 @@ go 1.25.5 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk/logging v0.48.3 + github.com/grafana/grafana-app-sdk/logging v0.48.7 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 go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/component-base v0.34.2 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + k8s.io/apimachinery v0.34.3 + k8s.io/apiserver v0.34.3 + k8s.io/component-base v0.34.3 k8s.io/klog/v2 v2.130.1 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) @@ -26,7 +26,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.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/felixge/httpsnoop v1.0.4 // indirect @@ -34,14 +34,24 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // 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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // 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 @@ -50,60 +60,56 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // 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.etcd.io/etcd/api/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect + go.etcd.io/etcd/client/v3 v3.6.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.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/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.2 // indirect - k8s.io/client-go v0.34.2 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 3eaa748643e..af28e90fadc 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -11,8 +11,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -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/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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= @@ -32,17 +32,42 @@ 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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -54,8 +79,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/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= @@ -71,8 +96,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/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.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= -github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw= +github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU= 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= @@ -85,8 +110,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLW github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= 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/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= @@ -99,8 +122,6 @@ 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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= @@ -129,11 +150,11 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: 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.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -158,12 +179,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.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.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= -go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= -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/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ= +go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4= +go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs= +go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI= +go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec= +go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8= 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= @@ -172,26 +193,26 @@ 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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/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/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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/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/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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -207,8 +228,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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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,30 +240,30 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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-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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -250,8 +271,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -260,20 +281,20 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -281,26 +302,26 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= +k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= +k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= 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/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 2bc514fcfd5..5170c87da05 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -10,14 +10,14 @@ 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 github.com/urfave/cli/v2 v2.27.7 // @grafana/grafana-backend-group - 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.47.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/sync v0.18.0 // @grafana/alerting-backend - golang.org/x/text v0.31.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.39.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/sdk v1.39.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.39.0 // indirect; @grafana/grafana-backend-group + golang.org/x/net v0.48.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/sync v0.19.0 // @grafana/alerting-backend + golang.org/x/text v0.32.0 // indirect; @grafana/grafana-backend-group google.golang.org/grpc v1.77.0 // indirect; @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.10 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.11 // indirect; @grafana/plugins-platform-backend ) require ( @@ -28,10 +28,10 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + golang.org/x/sys v0.39.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect ) require ( @@ -46,6 +46,7 @@ require ( github.com/Khan/genqlient v0.8.1 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect @@ -56,12 +57,12 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp 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/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index cdd5828ebd8..ae3e2125dc5 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -12,6 +12,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 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/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -61,8 +63,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -71,48 +73,48 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9 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/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= 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.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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 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/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= -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/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index b3ac434ea53..fc7aa317583 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.39.0 + golang.org/x/tools v0.40.0 ) require ( - golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect ) diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index bd76c016a3e..7091ed41da2 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 7d01f823e78..f608e1fb1c1 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -9,18 +9,18 @@ require ( github.com/grafana/cog v0.0.46 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 - golang.org/x/tools v0.39.0 + golang.org/x/tools v0.40.0 ) require ( github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/dave/jennifer v1.7.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/proto v1.13.2 // indirect - github.com/expr-lang/expr v1.17.6 // indirect + github.com/emicklei/proto v1.14.2 // indirect + github.com/expr-lang/expr v1.17.7 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/golang/glog v1.2.5 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -29,9 +29,10 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect @@ -39,18 +40,19 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 5e2bca5c947..a5303120de5 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -9,16 +9,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= -github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= +github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 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-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -47,6 +49,7 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -57,8 +60,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -73,6 +76,7 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -81,6 +85,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= @@ -94,22 +99,22 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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/infra/log/log.go b/pkg/infra/log/log.go index 9e7a51a8dc7..abae387729e 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -54,7 +54,8 @@ func init() { } logger := level.NewFilter(format(os.Stderr), level.AllowInfo()) root = newManager(logger) - initAppSDKLogger(logger) + // Use default Info level during package initialization before config is loaded + initAppSDKLogger(logger, slog.LevelInfo) RegisterContextualLogProvider(func(ctx context.Context) ([]any, bool) { pFromCtx := ctx.Value(logParamsContextKey{}) @@ -80,7 +81,7 @@ func newManager(logger gokitlog.Logger) *logManager { } } -func (lm *logManager) initialize(loggers []logWithFilters) { +func (lm *logManager) initialize(loggers []logWithFilters, levelStr string) { lm.mutex.Lock() defer lm.mutex.Unlock() @@ -113,7 +114,7 @@ func (lm *logManager) initialize(loggers []logWithFilters) { lm.loggersByName[name].Swap(&compositeLogger{loggers: ctxLoggers}) } - initAppSDKLogger(lm.ConcreteLogger) + initAppSDKLogger(lm.ConcreteLogger, stringToSlogLevel(levelStr)) } func (lm *logManager) New(ctx ...any) *ConcreteLogger { @@ -514,7 +515,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { configLoggers = append(configLoggers, handler) } if len(configLoggers) > 0 { - root.initialize(configLoggers) + root.initialize(configLoggers, defaultLevelName) } return nil @@ -551,8 +552,22 @@ func SetupConsoleLogger(level string) error { return nil } -func initAppSDKLogger(gkl gokitlog.Logger) { - // We need to allow Debug logs here. go-kit/log does not support sharing the level we're using. - // TODO: Refactor such that we can pass in a level in a more appropriate manner. - logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, slog.LevelDebug)) +// stringToSlogLevel converts a log level string to slog.Level +func stringToSlogLevel(levelStr string) slog.Level { + switch strings.ToLower(levelStr) { + case "trace", "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn", "warning": + return slog.LevelWarn + case "error", "critical": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func initAppSDKLogger(gkl gokitlog.Logger, level slog.Level) { + logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, level)) } diff --git a/pkg/infra/log/log_test.go b/pkg/infra/log/log_test.go index 98fee4be8c0..5302e565964 100644 --- a/pkg/infra/log/log_test.go +++ b/pkg/infra/log/log_test.go @@ -88,7 +88,7 @@ func TestNew(t *testing.T) { val: swapLogger, maxLevel: level.AllowAll(), }, - }) + }, "info") err := log1.Log("msg", "hello 1") require.NoError(t, err) diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index c1995545df8..19113ed3b1f 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -224,7 +224,7 @@ var ( MStatTotalRepositories prometheus.Gauge // MUnifiedStorageMigrationStatus indicates the migration status for unified storage in this instance. - // Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run). + // Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run), 3 (migration will run). MUnifiedStorageMigrationStatus prometheus.Gauge ) diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index fd8dbae7b77..7d2a0ba5439 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -204,6 +204,9 @@ type Panel struct { Options map[string]any `json:"options,omitempty"` // Field options allow you to change how the data is displayed in your visualizations. FieldConfig *FieldConfigSource `json:"fieldConfig,omitempty"` + // When a panel is migrated from a previous version (Angular to React), this field is set to the original panel type. + // This is used to determine the original panel type when migrating to a new version so the plugin migration can be applied. + AutoMigrateFrom *string `json:"autoMigrateFrom,omitempty"` } // NewPanel creates a new Panel object. @@ -834,6 +837,8 @@ type VariableModel struct { // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. Regex *string `json:"regex,omitempty"` + // Determine whether regex applies to variable value or display text + RegexApplyTo *VariableRegexApplyTo `json:"regexApplyTo,omitempty"` // Additional static options for query variable StaticOptions []VariableOption `json:"staticOptions,omitempty"` // Ordering of static options in relation to options returned from data source for query variable @@ -942,6 +947,15 @@ const ( VariableSortNaturalDesc VariableSort = 8 ) +// Determine whether regex applies to variable value or display text +// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) +type VariableRegexApplyTo string + +const ( + VariableRegexApplyToValue VariableRegexApplyTo = "value" + VariableRegexApplyToText VariableRegexApplyTo = "text" +) + // Contains the list of annotations that are associated with the dashboard. // Annotations are used to overlay event markers and overlay event tags on graphs. // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 81c7cd31f9d..b94f0514879 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -81,7 +81,15 @@ func (s *SocialGoogle) Validate(ctx context.Context, newSettings ssoModels.SSOSe return validation.Validate(info, requester, validation.MustBeEmptyValidator(info.AuthUrl, "Auth URL"), validation.MustBeEmptyValidator(info.TokenUrl, "Token URL"), - validation.MustBeEmptyValidator(info.ApiUrl, "API URL")) + validation.MustBeEmptyValidator(info.ApiUrl, "API URL"), + loginPromptValidator) +} + +func loginPromptValidator(info *social.OAuthInfo, requester identity.Requester) error { + if info.UseRefreshToken && !slices.Contains([]string{"", "consent"}, info.LoginPrompt) { + return ssosettings.ErrInvalidOAuthConfig("If provided, login_prompt must be set to consent when use_refresh_token is enabled.") + } + return nil } func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { diff --git a/pkg/login/social/connectors/google_oauth_test.go b/pkg/login/social/connectors/google_oauth_test.go index d330c39d78d..620c448013b 100644 --- a/pkg/login/social/connectors/google_oauth_test.go +++ b/pkg/login/social/connectors/google_oauth_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" "testing" "time" @@ -18,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -871,6 +873,39 @@ func TestSocialGoogle_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if use_refresh_token is enabled and login prompt is neither empty or 'consent'", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "use_refresh_token": "true", + "login_prompt": "login", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, + { + name: "succeeds if use_refresh_token is enabled and login prompt is empty", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "use_refresh_token": "true", + "login_prompt": "", + }, + }, + wantErr: nil, + }, + { + name: "succeeds if use_refresh_token is enabled and login prompt is consent", + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "use_refresh_token": "true", + "login_prompt": "consent", + }, + }, + wantErr: nil, + }, } for _, tc := range testCases { @@ -886,7 +921,13 @@ func TestSocialGoogle_Validate(t *testing.T) { require.ErrorIs(t, err, tc.wantErr) return } - require.NoError(t, err) + + if err != nil { + var e errutil.Error + require.True(t, errors.As(err, &e)) + require.NoError(t, e, "expected no error, got %v", e.PublicMessage) + return + } }) } } @@ -1024,3 +1065,102 @@ func TestIsHDAllowed(t *testing.T) { }) } } + +func TestSocialGoogle_AuthCodeURL(t *testing.T) { + testCases := []struct { + name string + info *social.OAuthInfo + opts []oauth2.AuthCodeOption + state string + wantURL *url.URL + }{ + { + name: "should return the correct auth code URL", + info: &social.OAuthInfo{ + ClientId: "client-id", + ClientSecret: "client-secret", + AuthUrl: "https://example.com/auth", + LoginPrompt: "login", + Scopes: []string{"openid", "email", "profile"}, + }, + state: "test-state", + opts: []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("extra_param", "extra_value"), + }, + wantURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/auth", + RawQuery: url.Values{ + "state": {"test-state"}, + "prompt": {"login"}, + "response_type": {"code"}, + "client_id": {"client-id"}, + "redirect_uri": {"/login/google"}, + "scope": {"openid email profile"}, + "extra_param": {"extra_value"}, + }.Encode(), + }, + }, + { + name: "should add access type offline and approval force if use refresh token is enabled", + info: &social.OAuthInfo{ + ClientId: "client-id", + ClientSecret: "client-secret", + AuthUrl: "https://example.com/auth", + Scopes: []string{"openid", "email", "profile"}, + UseRefreshToken: true, + }, + state: "test-state", + wantURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/auth", + RawQuery: url.Values{ + "state": {"test-state"}, + "prompt": {"consent"}, + "response_type": {"code"}, + "client_id": {"client-id"}, + "redirect_uri": {"/login/google"}, + "scope": {"openid email profile"}, + "access_type": {"offline"}, + }.Encode(), + }, + }, + { + name: "should override configured login prompt if use refresh token is enabled", + info: &social.OAuthInfo{ + ClientId: "client-id", + ClientSecret: "client-secret", + AuthUrl: "https://example.com/auth", + Scopes: []string{"openid", "email", "profile"}, + UseRefreshToken: true, + }, + state: "test-state", + wantURL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/auth", + RawQuery: url.Values{ + "state": {"test-state"}, + "prompt": {"consent"}, + "response_type": {"code"}, + "client_id": {"client-id"}, + "redirect_uri": {"/login/google"}, + "scope": {"openid email profile"}, + "access_type": {"offline"}, + }.Encode(), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := NewGoogleProvider(tc.info, &setting.Cfg{}, nil, ssosettingstests.NewFakeService(), featuremgmt.WithFeatures()) + gotURL := s.AuthCodeURL(tc.state, tc.opts...) + parsedURL, err := url.Parse(gotURL) + require.NoError(t, err) + require.EqualValues(t, tc.wantURL, parsedURL) + }) + } +} diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 0db4d81baeb..3ecbf8e334e 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -91,7 +91,11 @@ func (s *SocialBase) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) st func (s *SocialBase) getAuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { if s.info.LoginPrompt != "" { promptOpt := oauth2.SetAuthURLParam("prompt", s.info.LoginPrompt) - opts = append(opts, promptOpt) + + // Prepend the prompt option to the opts slice to ensure it is applied last. + // This is necessary in case the caller provides an option that overrides the prompt, + // such as `oauth2.ApprovalForce`. + opts = append([]oauth2.AuthCodeOption{promptOpt}, opts...) } return s.Config.AuthCodeURL(state, opts...) diff --git a/pkg/middleware/loggermw/logger.go b/pkg/middleware/loggermw/logger.go index 1255d87aa0e..1b4f3d4d2d4 100644 --- a/pkg/middleware/loggermw/logger.go +++ b/pkg/middleware/loggermw/logger.go @@ -64,10 +64,7 @@ func (l *loggerImpl) Middleware() web.Middleware { // put the start time on context so we can measure it later. r = r.WithContext(log.InitstartTime(r.Context(), time.Now())) - //nolint:staticcheck // not yet migrated to OpenFeature - if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) { - r = r.WithContext(errutil.SetUnifiedLogging(r.Context())) - } + r = r.WithContext(errutil.SetUnifiedLogging(r.Context())) rw := web.Rw(w, r) next.ServeHTTP(rw, r) diff --git a/pkg/middleware/request_tracing.go b/pkg/middleware/request_tracing.go index 998b20d7dbb..c06142a936a 100644 --- a/pkg/middleware/request_tracing.go +++ b/pkg/middleware/request_tracing.go @@ -73,16 +73,20 @@ func RouteOperationName(req *http.Request) (string, bool) { return "", false } -// Paths that don't need tracing spans applied to them because of the -// little value that would provide us -func SkipTracingPaths(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/public/") || +func ShouldTraceWithExceptions(req *http.Request) bool { + // Paths that don't need tracing spans applied to them because of the + // little value that would provide us + if strings.HasPrefix(req.URL.Path, "/public/") || req.URL.Path == "/robots.txt" || req.URL.Path == "/favicon.ico" || - req.URL.Path == "/api/health" + req.URL.Path == "/api/health" { + return false + } + + return true } -func TraceAllPaths(req *http.Request) bool { +func ShouldTraceAllPaths(req *http.Request) bool { return true } diff --git a/pkg/operators/iam/zanzana_folder_reconciler.go b/pkg/operators/iam/zanzana_folder_reconciler.go index 0f2fb287bb4..ee2233d7877 100644 --- a/pkg/operators/iam/zanzana_folder_reconciler.go +++ b/pkg/operators/iam/zanzana_folder_reconciler.go @@ -11,15 +11,16 @@ import ( "os/signal" "syscall" + "github.com/prometheus/client_golang/prometheus" + "k8s.io/client-go/rest" + "k8s.io/client-go/transport" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/apps/iam/pkg/app" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "k8s.io/client-go/rest" - "k8s.io/client-go/transport" "github.com/grafana/authlib/authn" utilnet "k8s.io/apimachinery/pkg/util/net" @@ -95,7 +96,7 @@ func buildIAMConfigFromSettings(cfg *setting.Cfg, registerer prometheus.Register if zanzanaURL == "" { return nil, fmt.Errorf("zanzana_url is required in [operator] section") } - iamCfg.AppConfig.ZanzanaClientCfg.URL = zanzanaURL + iamCfg.AppConfig.ZanzanaClientCfg.Addr = zanzanaURL iamCfg.AppConfig.InformerConfig.MaxConcurrentWorkers = operatorSec.Key("max_concurrent_workers").MustUint64(20) diff --git a/pkg/operators/provisioning/config.go b/pkg/operators/provisioning/config.go index 8e496e5e556..05552e56095 100644 --- a/pkg/operators/provisioning/config.go +++ b/pkg/operators/provisioning/config.go @@ -178,7 +178,7 @@ func setupFromConfig(cfg *setting.Cfg, registry prometheus.Registerer) (controll APIPath: "/apis", Host: url, WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { - return authrt.NewRoundTripper(tokenExchangeClient, rt, group) + return authrt.NewRoundTripper(tokenExchangeClient, rt, group, authrt.ExtraAudience(provisioning.GROUP)) }), Transport: &http.Transport{ MaxConnsPerHost: 100, diff --git a/pkg/operators/provisioning/repo_operator.go b/pkg/operators/provisioning/repo_operator.go index 1651a126ffe..416eb5c0e3f 100644 --- a/pkg/operators/provisioning/repo_operator.go +++ b/pkg/operators/provisioning/repo_operator.go @@ -85,7 +85,6 @@ func RunRepoController(deps server.OperatorDependencies) error { resourceLister, controllerCfg.clients, jobs, - nil, // dualwrite -- standalone operator assumes it is backed by unified storage healthChecker, statusPatcher, deps.Registerer, diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index b4e296c934a..641cf9f9b49 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -16,11 +16,11 @@ require ( cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/dave/dst v0.27.3 // indirect - github.com/emicklei/proto v1.13.2 // indirect - github.com/expr-lang/expr v1.17.6 // indirect + github.com/emicklei/proto v1.14.2 // indirect + github.com/expr-lang/expr v1.17.7 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -28,7 +28,7 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect @@ -37,17 +37,18 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.33.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 635c691a7be..a514789135c 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -10,16 +10,18 @@ github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEy github.com/davecgh/go-spew v1.1.0/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/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= -github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= -github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= +github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= +github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 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-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= @@ -51,8 +53,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -76,8 +78,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= -github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= 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/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= @@ -88,26 +90,28 @@ 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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index 0fb1217ec70..e6845322516 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -3,11 +3,16 @@ package bootstrap import ( "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/semconv" ) // Bootstrapper is responsible for the Bootstrap stage of the plugin loader pipeline. @@ -34,6 +39,7 @@ type Bootstrap struct { constructStep ConstructFunc decorateSteps []DecorateFunc log log.Logger + tracer trace.Tracer } type Opts struct { @@ -55,14 +61,21 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap { constructStep: opts.ConstructFunc, decorateSteps: opts.DecorateFuncs, log: log.New("plugins.bootstrap"), + tracer: otel.Tracer("github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"), } } // Bootstrap will execute the Construct and Decorate steps of the Bootstrap stage. func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, found *plugins.FoundBundle) ([]*plugins.Plugin, error) { + pluginClass := src.PluginClass(ctx) + ctx, span := b.tracer.Start(ctx, "bootstrap.Bootstrap", trace.WithAttributes( + semconv.PluginSourceClass(pluginClass), + )) + defer span.End() + ps, err := b.constructStep(ctx, src, found) if err != nil { - return nil, err + return nil, tracing.Error(span, err) } if len(b.decorateSteps) == 0 { @@ -76,7 +89,7 @@ func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, fou ip, err = decorate(ctx, p) if err != nil { b.log.Error("Could not decorate plugin", "pluginId", p.ID, "error", err) - return nil, err + return nil, tracing.Error(span, err) } } bootstrappedPlugins = append(bootstrappedPlugins, ip) diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go index e624fbb3fcc..e5bdc50dd62 100644 --- a/pkg/plugins/manager/pipeline/discovery/discovery.go +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -3,6 +3,11 @@ package discovery import ( "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" @@ -26,6 +31,7 @@ type FilterFunc func(ctx context.Context, class plugins.Class, bundles []*plugin type Discovery struct { filterSteps []FilterFunc log log.Logger + tracer trace.Tracer } type Opts struct { @@ -41,29 +47,37 @@ func New(_ *config.PluginManagementCfg, opts Opts) *Discovery { return &Discovery{ filterSteps: opts.FilterFuncs, log: log.New("plugins.discovery"), + tracer: otel.Tracer("github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"), } } // Discover will execute the Filter step of the Discovery stage. func (d *Discovery) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + pluginClass := src.PluginClass(ctx) + ctx, span := d.tracer.Start(ctx, "discovery.Discover", trace.WithAttributes( + attribute.String("grafana.plugins.class", string(pluginClass)), + )) + defer span.End() + ctxLogger := d.log.FromContext(ctx) + // Use the source's own Discover method found, err := src.Discover(ctx) if err != nil { - d.log.Warn("Discovery source failed", "class", src.PluginClass(ctx), "error", err) - return nil, err + ctxLogger.Warn("Discovery source failed", "class", pluginClass, "error", err) + return nil, tracing.Error(span, err) } - d.log.Debug("Found plugins", "class", src.PluginClass(ctx), "count", len(found)) + ctxLogger.Debug("Found plugins", "class", pluginClass, "count", len(found)) // Apply filtering steps result := found for _, filter := range d.filterSteps { result, err = filter(ctx, src.PluginClass(ctx), result) if err != nil { - return nil, err + return nil, tracing.Error(span, err) } } - d.log.Debug("Discovery complete", "class", src.PluginClass(ctx), "found", len(found), "filtered", len(result)) + ctxLogger.Debug("Discovery complete", "class", pluginClass, "found", len(found), "filtered", len(result)) return result, nil } diff --git a/pkg/plugins/manager/pipeline/initialization/initialization.go b/pkg/plugins/manager/pipeline/initialization/initialization.go index 52a638a00f0..4319f4811a7 100644 --- a/pkg/plugins/manager/pipeline/initialization/initialization.go +++ b/pkg/plugins/manager/pipeline/initialization/initialization.go @@ -3,9 +3,14 @@ package initialization import ( "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/semconv" ) // Initializer is responsible for the Initialization stage of the plugin loader pipeline. @@ -20,6 +25,7 @@ type Initialize struct { cfg *config.PluginManagementCfg initializeSteps []InitializeFunc log log.Logger + tracer trace.Tracer } type Opts struct { @@ -36,11 +42,17 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Initialize { cfg: cfg, initializeSteps: opts.InitializeFuncs, log: log.New("plugins.initialization"), + tracer: otel.Tracer("github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"), } } // Initialize will execute the Initialize steps of the Initialization stage. func (i *Initialize) Initialize(ctx context.Context, ps *plugins.Plugin) (*plugins.Plugin, error) { + ctx, span := i.tracer.Start(ctx, "initialization.Initialize", trace.WithAttributes( + semconv.GrafanaPluginId(ps.ID), + )) + defer span.End() + if len(i.initializeSteps) == 0 { return ps, nil } @@ -51,7 +63,7 @@ func (i *Initialize) Initialize(ctx context.Context, ps *plugins.Plugin) (*plugi ip, err = init(ctx, ps) if err != nil { i.log.Error("Could not initialize plugin", "pluginId", ps.ID, "error", err) - return nil, err + return nil, tracing.Error(span, err) } } diff --git a/pkg/plugins/manager/pipeline/termination/termination.go b/pkg/plugins/manager/pipeline/termination/termination.go index 66e76d8a548..fdb28396bbf 100644 --- a/pkg/plugins/manager/pipeline/termination/termination.go +++ b/pkg/plugins/manager/pipeline/termination/termination.go @@ -3,9 +3,14 @@ package termination import ( "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/semconv" ) // Terminator is responsible for the Termination stage of the plugin loader pipeline. @@ -20,6 +25,7 @@ type Terminate struct { cfg *config.PluginManagementCfg terminateSteps []TerminateFunc log log.Logger + tracer trace.Tracer } type Opts struct { @@ -36,14 +42,20 @@ func New(cfg *config.PluginManagementCfg, opts Opts) (*Terminate, error) { cfg: cfg, terminateSteps: opts.TerminateFuncs, log: log.New("plugins.termination"), + tracer: otel.Tracer("github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"), }, nil } // Terminate will execute the Terminate steps of the Termination stage. func (t *Terminate) Terminate(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + ctx, span := t.tracer.Start(ctx, "termination.Terminate", trace.WithAttributes( + semconv.GrafanaPluginId(p.ID), + )) + defer span.End() + for _, terminate := range t.terminateSteps { if err := terminate(ctx, p); err != nil { - return nil, err + return nil, tracing.Error(span, err) } } return p, nil diff --git a/pkg/plugins/manager/pipeline/validation/validation.go b/pkg/plugins/manager/pipeline/validation/validation.go index 9ea7be9b3a7..36db1f25163 100644 --- a/pkg/plugins/manager/pipeline/validation/validation.go +++ b/pkg/plugins/manager/pipeline/validation/validation.go @@ -3,9 +3,14 @@ package validation import ( "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/semconv" ) // Validator is responsible for the Validation stage of the plugin loader pipeline. @@ -20,6 +25,7 @@ type Validate struct { cfg *config.PluginManagementCfg validateSteps []ValidateFunc log log.Logger + tracer trace.Tracer } type Opts struct { @@ -36,11 +42,17 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Validate { cfg: cfg, validateSteps: opts.ValidateFuncs, log: log.New("plugins.validation"), + tracer: otel.Tracer("github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"), } } // Validate will execute the Validate steps of the Validation stage. func (v *Validate) Validate(ctx context.Context, ps *plugins.Plugin) error { + ctx, span := v.tracer.Start(ctx, "validation.Validate", trace.WithAttributes( + semconv.GrafanaPluginId(ps.ID), + )) + defer span.End() + if len(v.validateSteps) == 0 { return nil } @@ -49,7 +61,7 @@ func (v *Validate) Validate(ctx context.Context, ps *plugins.Plugin) error { err := validate(ctx, ps) if err != nil { v.log.Error("Plugin validation failed", "pluginId", ps.ID, "error", err) - return err + return tracing.Error(span, err) } } diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index efbcd9222a6..ae8439e8663 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -8,13 +8,13 @@ require ( github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.3 + github.com/prometheus/common v0.67.4 github.com/prometheus/prometheus v0.303.1 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - google.golang.org/protobuf v1.36.10 - k8s.io/apimachinery v0.34.2 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + google.golang.org/protobuf v1.36.11 + k8s.io/apimachinery v0.34.3 ) require ( @@ -36,16 +36,26 @@ require ( github.com/fxamacker/cbor/v2 v2.9.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.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // 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 @@ -61,11 +71,10 @@ require ( github.com/hashicorp/yamux v0.1.2 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect - github.com/josharian/intern v1.0.0 // 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.3.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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 @@ -80,47 +89,48 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.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/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc 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/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/atomic v1.11.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.45.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.242.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/grpc v1.77.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.34.2 // indirect + k8s.io/client-go v0.34.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 9810bb19828..eec9ab3dffd 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -66,36 +66,46 @@ github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7Bd 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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= 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/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.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -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/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/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -114,8 +124,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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= @@ -162,8 +172,6 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -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= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -184,8 +192,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -239,10 +247,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= @@ -280,34 +288,34 @@ go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFX go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -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/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= 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/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= 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.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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -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/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -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/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -321,27 +329,27 @@ 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -352,22 +360,22 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -378,14 +386,14 @@ 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.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-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -396,18 +404,18 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 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= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= diff --git a/pkg/registry/apis/collections/legacy/stars.go b/pkg/registry/apis/collections/legacy/stars.go index 8c7d9e711ea..39f7a9c088f 100644 --- a/pkg/registry/apis/collections/legacy/stars.go +++ b/pkg/registry/apis/collections/legacy/stars.go @@ -204,11 +204,14 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *collections.Star previous[v] = true } } - for _, dashboard := range stars { + for idx, dashboard := range stars { if previous[dashboard] { delete(previous, dashboard) continue // nothing needed } + if idx > 0 { + time.Sleep(75 * time.Millisecond) // values are ordered by update time; this keeps the order predictable + } err = s.stars.Add(ctx, &star.StarDashboardCommand{ UserID: user.ID, OrgID: user.OrgID, diff --git a/pkg/registry/apis/collections/register.go b/pkg/registry/apis/collections/register.go index 9be4412c377..699d3bbe672 100644 --- a/pkg/registry/apis/collections/register.go +++ b/pkg/registry/apis/collections/register.go @@ -46,12 +46,6 @@ func RegisterAPIService( users user.Service, apiregistration builder.APIRegistrar, ) *APIBuilder { - // Requires development settings and clearly experimental - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil - } - sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db)) builder := &APIBuilder{ authorizer: &utils.AuthorizeFromName{ diff --git a/pkg/registry/apis/dashboard/dashboard_storage.go b/pkg/registry/apis/dashboard/dashboard_storage.go index d2d54c8d815..8bde214129b 100644 --- a/pkg/registry/apis/dashboard/dashboard_storage.go +++ b/pkg/registry/apis/dashboard/dashboard_storage.go @@ -7,20 +7,45 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/live" ) -// dashboardStoragePermissionWrapper is a wrapper around the grafanarest.Storage that adds dashboard permissions handling -// when dual writing is enabled. -type dashboardStoragePermissionWrapper struct { - dashboardPermissionsSvc accesscontrol.DashboardPermissionsService +// dashboardStorageWrapper is a wrapper around the grafanarest.Storage so it will: +// 1. support adds dashboard permissions handling +// 2. broadcast changes to grafana live +// when running in single tenant mode +type dashboardStorageWrapper struct { grafanarest.Storage + + dashboardPermissionsSvc accesscontrol.DashboardPermissionsService + live live.DashboardActivityChannel } -func (d dashboardStoragePermissionWrapper) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - info, err := request.NamespaceInfoFrom(ctx, true) +func (d dashboardStorageWrapper) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + obj, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) + if err == nil && ns.OrgID > 0 && d.live != nil { + m, err := utils.MetaAccessor(obj) + if err == nil { + if err := d.live.DashboardSaved(ns.OrgID, name, m.GetResourceVersion()); err != nil { + logging.FromContext(ctx).Info("live dashboard update failed", "err", err) + } + } + } + return obj, created, err +} + +func (d dashboardStorageWrapper) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, false, err } @@ -28,7 +53,12 @@ func (d dashboardStoragePermissionWrapper) Delete(ctx context.Context, name stri if err != nil { return obj, async, err } - if accessErr := d.dashboardPermissionsSvc.DeleteResourcePermissions(ctx, info.OrgID, name); accessErr != nil { + if ns.OrgID > 0 && d.live != nil { + if err := d.live.DashboardDeleted(ns.OrgID, name); err != nil { + logging.FromContext(ctx).Info("live dashboard update failed", "err", err) + } + } + if accessErr := d.dashboardPermissionsSvc.DeleteResourcePermissions(ctx, ns.OrgID, name); accessErr != nil { return obj, async, accessErr } return obj, async, nil diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index 913dd02d14d..258d0d1a519 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -95,3 +95,7 @@ func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.Cal func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { return nil, fmt.Errorf("not implemented") } + +func (b *directResourceClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/registry/apis/dashboard/legacy/queries_test.go b/pkg/registry/apis/dashboard/legacy/queries_test.go index 1345bfc2925..ec865873960 100644 --- a/pkg/registry/apis/dashboard/legacy/queries_test.go +++ b/pkg/registry/apis/dashboard/legacy/queries_test.go @@ -100,6 +100,42 @@ func TestDashboardQueries(t *testing.T) { Order: "ASC", }), }, + { + // Tests that MaxRows generates LIMIT clause for regular dashboard queries + Name: "dashboard_with_max_rows", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + }), + }, + { + // Tests that MaxRows generates LIMIT clause for history queries + Name: "history_with_max_rows", + Data: getQuery(&DashboardQuery{ + OrgID: 1, + GetHistory: true, + MaxRows: 50, + }), + }, + { + // Tests that MaxRows + LastID generates correct pagination query + Name: "dashboard_with_max_rows_last_id", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + LastID: 500, + }), + }, + { + // Tests that MaxRows + LastID generates correct pagination query + Name: "history_with_max_rows_last_id", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + GetHistory: true, + LastID: 500, + }), + }, }, sqlQueryPanels: { { diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql index e8503170a2c..b32c5aa3465 100644 --- a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -71,6 +71,9 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} COALESCE(dashboard_version.version, dashboard.version) {{ .Query.Order }}, {{ end }} dashboard.uid ASC + {{ if .Query.MaxRows }} + LIMIT {{ .Arg .Query.MaxRows }} + {{ end }} {{ else }} {{ if .Query.UID }} AND dashboard.uid = {{ .Arg .Query.UID }} @@ -83,4 +86,7 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} AND dashboard.deleted IS NULL {{ end }} ORDER BY dashboard.id DESC + {{ if .Query.MaxRows }} + LIMIT {{ .Arg .Query.MaxRows }} + {{ end }} {{ end }} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 24ec135e785..0b7ffa04ea4 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -113,6 +113,7 @@ func ProvideMigratorDashboardAccessor( dashboardPermissionSvc: nil, // not needed for migration libraryPanelSvc: nil, // not needed for migration accessControl: accessControl, + log: log.New("legacy.dashboard.migrator.accessor"), } } @@ -136,15 +137,29 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc: dashboardPermissionSvc, libraryPanelSvc: libraryPanelSvc, accessControl: accessControl, + log: log.New("legacy.dashboard.accessor"), } } func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query string, args ...any) (*sql.Rows, error) { - // Use transaction if available in context. - // This allows us to run migrations in a transaction which is specifically required for SQLite. - if tx := resource.TransactionFromContext(ctx); tx != nil { + var tx *sql.Tx + // After this function runs, the `tx` variable will only be set if + // this function was called in the context of a transaction set up by a + // caller upstream. In that case, we reuse the transaction. + _ = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + coreTx, err := sess.Tx() + if err != nil { + return nil + } + + tx = coreTx.Tx + return nil + }) + + if tx != nil { return tx.QueryContext(ctx, query, args...) } + return helper.DB.GetSqlxSession().Query(ctx, query, args...) } @@ -488,7 +503,7 @@ func (a *dashboardSqlAccess) MigratePlaylists(ctx context.Context, orgId int64, return nil, err } - // Group playlist items by playlist ID + // Group playlist items by playlist ID while preserving order type playlistData struct { id int64 uid string @@ -499,7 +514,8 @@ func (a *dashboardSqlAccess) MigratePlaylists(ctx context.Context, orgId int64, updatedAt int64 } - playlists := make(map[int64]*playlistData) + playlistIndex := make(map[int64]int) // maps playlist ID to index in playlists slice + playlists := []*playlistData{} var currentID int64 var orgID int64 var uid, name, interval string @@ -514,7 +530,8 @@ func (a *dashboardSqlAccess) MigratePlaylists(ctx context.Context, orgId int64, } // Get or create playlist entry - pl, exists := playlists[currentID] + idx, exists := playlistIndex[currentID] + var pl *playlistData if !exists { pl = &playlistData{ id: currentID, @@ -525,7 +542,10 @@ func (a *dashboardSqlAccess) MigratePlaylists(ctx context.Context, orgId int64, createdAt: createdAt, updatedAt: updatedAt, } - playlists[currentID] = pl + playlistIndex[currentID] = len(playlists) + playlists = append(playlists, pl) + } else { + pl = playlists[idx] } // Add item if it exists (LEFT JOIN can return NULL for playlists without items) @@ -541,7 +561,7 @@ func (a *dashboardSqlAccess) MigratePlaylists(ctx context.Context, orgId int64, return nil, err } - // Convert to K8s objects and send to stream + // Convert to K8s objects and send to stream (order is preserved) for _, pl := range playlists { playlist := &playlistv0.Playlist{ TypeMeta: metav1.TypeMeta{ @@ -693,6 +713,128 @@ func (r *rowsWrapper) Value() []byte { return b } +// batchingIterator wraps rowsWrapper to fetch data in batches +type batchingIterator struct { + wrapper *rowsWrapper + a *dashboardSqlAccess + ctx context.Context + helper *legacysql.LegacyDatabaseHelper + query *DashboardQuery + batchSize int + done bool + err error +} + +var _ resource.ListIterator = (*batchingIterator)(nil) + +func (b *batchingIterator) Error() error { + if b.err != nil { + return b.err + } + return b.wrapper.Error() +} + +func (b *batchingIterator) ContinueToken() string { + return b.wrapper.ContinueToken() +} + +func (b *batchingIterator) ResourceVersion() int64 { + return b.wrapper.ResourceVersion() +} + +func (b *batchingIterator) Namespace() string { + return b.wrapper.Namespace() +} + +func (b *batchingIterator) Name() string { + return b.wrapper.Name() +} + +func (b *batchingIterator) Folder() string { + return b.wrapper.Folder() +} + +func (b *batchingIterator) Value() []byte { + return b.wrapper.Value() +} + +func (b *batchingIterator) Close() error { + return b.wrapper.Close() +} + +func newBatchingIterator(ctx context.Context, a *dashboardSqlAccess, helper *legacysql.LegacyDatabaseHelper, query *DashboardQuery) (*batchingIterator, error) { + iter := &batchingIterator{ + a: a, + ctx: ctx, + helper: helper, + query: query, + batchSize: query.MaxRows, + } + + // Loads the first batch + if err := iter.nextBatch(query.LastID); err != nil { + return nil, err + } + return iter, nil +} + +func (b *batchingIterator) nextBatch(lastID int64) error { + b.query.LastID = lastID + wrapper, err := b.a.getRows(b.ctx, b.helper, b.query) + if err != nil { + return err + } + b.wrapper = wrapper + return nil +} + +func (b *batchingIterator) Next() bool { + if b.done { + return false + } + + // Try to get next row from current batch + if b.wrapper.Next() { + return true + } + + // Check for errors in current wrapper + if b.Error() != nil { + return false + } + + // No more rows in current batch - close it + if err := b.wrapper.Close(); err != nil { + // Should not happen, but handle it + b.err = err + b.done = true + return false + } + + // Current batch exhausted - check if we got a full batch (might be more data) + if b.wrapper.count < b.batchSize { + // Got fewer rows than batch size, so we're done + b.done = true + return false + } + + // Fetch next batch with LastID from last row + if err := b.nextBatch(b.wrapper.row.token.id); err != nil { + b.err = err + b.done = true + return false + } + + // Try to get first row from new batch + if b.wrapper.Next() { + return true + } + + // New batch is empty, we're done + b.done = true + return false +} + func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) { generatedDashboard := map[string]interface{}{ "editable": true, diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index d8c948c9664..3d4eb6dcd91 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -132,10 +132,19 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ } } else { failOnExisting := event.Type == resourcepb.WatchEvent_ADDED - after, _, err := a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + sql, err := a.sql(ctx) if err != nil { return 0, err } + + var after *dashboard.Dashboard + if err := sql.DB.InTransaction(ctx, func(ctx context.Context) error { + var err error + after, _, err = a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + return err + }); err != nil { + return 0, err + } if after != nil { meta, err := utils.MetaAccessor(after) if err != nil { @@ -296,11 +305,19 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resourcepb.L return 0, fmt.Errorf("token and orgID mismatch") } + // Default batch size for iterator - fetch rows in batches to avoid slow queries + const defaultMaxRows = 500 + maxRows := defaultMaxRows + if req.Limit > 0 && req.Limit < int64(defaultMaxRows) { + maxRows = int(req.Limit) + } + query := &DashboardQuery{ - OrgID: info.OrgID, - Limit: int(req.Limit), - LastID: token.id, - Labels: req.Options.Labels, + OrgID: info.OrgID, + Limit: int(req.Limit), + MaxRows: maxRows, + LastID: token.id, + Labels: req.Options.Labels, } sql, err := a.sql(ctx) @@ -323,14 +340,15 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resourcepb.L return 0, err } listRV *= 1000 // Convert to microseconds - rows, err := a.getRows(ctx, sql, query) - if rows != nil { + + iter, err := newBatchingIterator(ctx, a, sql, query) + if iter != nil { defer func() { - _ = rows.Close() + _ = iter.Close() }() } if err == nil { - err = cb(rows) + err = cb(iter) } return listRV, err } diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2470760c9e3 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..d6c5ce10313 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..96efe4956a9 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..f5934ab44d1 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2cdb3115aec --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..6994bf56bd4 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..18a557c9dfd --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..b1930157b6e --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2cdb3115aec --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..6994bf56bd4 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..18a557c9dfd --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..b1930157b6e --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index 26cd043e8e5..968b2eb123c 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -16,6 +16,11 @@ type DashboardQuery struct { UID string // to select a single dashboard Limit int + // MaxRows is used internally by the iterator to fetch data in batches + // When set, the SQL query will include LIMIT MaxRows + // If Limit is smaller, that will be used instead + MaxRows int + // Included in the continue token // This is the ID from the last dashboard sent in the previous page LastID int64 diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index 9782ce6326d..2ad9c11582a 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -360,7 +360,17 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso }) } - list.TotalHits = int64(len(list.Results.Rows)) + // the UI expects us to populate "TotalHits" with the total search hits, not however many we are returning. + // this is a dumb workaround due to the fact that legacy doesn't expose a way of "counting search hits" + // it fixes a bug that only happens in mode 1-2 that prevents pagination from working in the dashboard list view + // we only have a handful of instances running in this mode and moving towards 0 instances fast, so this is fine + query.Limit = 0 + query.Page = 1 + res, err = c.dashboardStore.FindDashboards(ctx, query) + if err != nil { + return nil, err + } + list.TotalHits = int64(len(res)) return list, nil } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index cb7ead459f6..22907e24c8f 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -48,6 +48,18 @@ func TestDashboardSearchClient_Search(t *testing.T) { {ID: 2, UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2", Tags: []string{}}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Type: "dash-db", + Sort: sorter, + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Tags: []string{"term"}}, + {ID: 2, UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2", Tags: []string{}}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -124,6 +136,17 @@ func TestDashboardSearchClient_Search(t *testing.T) { {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50), Tags: []string{}}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Type: "dash-db", + Sort: sortOptionAsc, + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50), Tags: []string{}}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -189,6 +212,17 @@ func TestDashboardSearchClient_Search(t *testing.T) { {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2), Tags: []string{}}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Type: "dash-db", + Sort: sortOptionAsc, + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2), Tags: []string{}}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -293,6 +327,17 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + Title: "test", + SignedInUser: user, + Type: "dash-db", + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -319,6 +364,18 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + Title: "test", + TitleExactMatch: true, + SignedInUser: user, + Type: "dash-db", + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -350,6 +407,17 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + DashboardIds: []int64{1, 2}, + SignedInUser: user, + Type: "dash-db", + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -383,6 +451,19 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + DashboardUIDs: []string{"uid1", "uid2"}, + Tags: []string{"tag1", "tag2"}, + FolderUIDs: []string{"general", "folder1"}, + SignedInUser: user, + Type: "dash-db", + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -532,6 +613,17 @@ func TestDashboardSearchClient_Search(t *testing.T) { {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, }, nil).Once() + // Second call for total count with Page: 1, Limit: 0 + mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{ + SignedInUser: user, + Sort: sort.SortAlphaAsc, + Type: "dash-db", + Limit: 0, + Page: 1, + }).Return([]dashboards.DashboardSearchProjection{ + {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -552,7 +644,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { t.Run("Should set correct sort field when sorting by views", func(t *testing.T) { mockStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ {ID: 1, UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", SortMeta: 100}, - }, nil).Once() + }, nil).Twice() // Will be called twice due to the total count call req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ @@ -584,7 +676,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { mockStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ {UID: "dashboard1", FolderUID: "folder1", ID: 1}, {UID: "dashboard2", FolderUID: "folder2", ID: 2}, - }, nil).Once() + }, nil).Twice() // Will be called twice due to the total count call req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index b95ae2519b5..78c49b28e8a 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -22,7 +22,7 @@ import ( ) func TestDashboardAPIBuilder_Mutate(t *testing.T) { - migration.Initialize(testutil.NewDataSourceProvider(testutil.StandardTestConfig), testutil.NewLibraryElementProvider()) + migration.Initialize(testutil.NewDataSourceProvider(testutil.StandardTestConfig), testutil.NewLibraryElementProvider(), migration.DefaultCacheTTL) tests := []struct { name string inputObj runtime.Object diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index e9925f0a3b7..eed79dd6f0d 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "maps" + "strconv" "strings" + "github.com/grafana/grafana/pkg/configprovider" "github.com/prometheus/client_golang/prometheus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -55,12 +57,12 @@ import ( "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/live" "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" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/apistore" @@ -121,11 +123,11 @@ type DashboardsAPIBuilder struct { snapshotService dashboardsnapshots.Service snapshotOptions dashv0.SnapshotSharingOptions namespacer request.NamespaceMapper + dashboardActivityChannel live.DashboardActivityChannel isStandalone bool // skips any handling including anything to do with legacy storage } func RegisterAPIService( - cfg *setting.Cfg, features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, dashboardService dashboards.DashboardService, @@ -150,7 +152,15 @@ func RegisterAPIService( libraryPanels libraryelements.Service, publicDashboardService publicdashboards.Service, snapshotService dashboardsnapshots.Service, + dashboardActivityChannel live.DashboardActivityChannel, + configProvider configprovider.ConfigProvider, ) *DashboardsAPIBuilder { + cfg, err := configProvider.Get(context.Background()) + if err != nil { + logging.DefaultLogger.Error("failed to load settings configuration instance", "stackId", cfg.StackID, "err", err) + return nil + } + dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter) @@ -184,6 +194,7 @@ func RegisterAPIService( snapshotService: snapshotService, snapshotOptions: snapshotOptions, namespacer: namespacer, + dashboardActivityChannel: dashboardActivityChannel, legacy: &DashboardStorage{ Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), DashboardService: dashboardService, @@ -195,13 +206,30 @@ func RegisterAPIService( datasourceService: datasourceService, }, &libraryElementIndexProvider{ libraryElementService: libraryPanels, - }) + }, cfg.DashboardSchemaMigrationCacheTTL) + + // For single-tenant deployments (indicated by StackID), preload the cache in the background + if cfg.StackID != "" { + // Single namespace for cloud stack + stackID, err := strconv.ParseInt(cfg.StackID, 10, 64) + if err == nil { + var nsInfo authlib.NamespaceInfo + nsInfo, err = authlib.ParseNamespace(authlib.CloudNamespaceFormatter(stackID)) + if err == nil { + migration.PreloadCacheInBackground([]authlib.NamespaceInfo{nsInfo}) + } + } + if err != nil { + logging.DefaultLogger.Error("failed to parse namespace for cache preloading", "stackId", cfg.StackID, "err", err) + } + } + apiregistration.RegisterAPI(builder) return builder } -func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceIndexProvider, libraryElementProvider schemaversion.LibraryElementIndexProvider, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface) *DashboardsAPIBuilder { - migration.Initialize(datasourceProvider, libraryElementProvider) +func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceIndexProvider, libraryElementProvider schemaversion.LibraryElementIndexProvider, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface, search *SearchHandler) *DashboardsAPIBuilder { + migration.Initialize(datasourceProvider, libraryElementProvider, migration.DefaultCacheTTL) return &DashboardsAPIBuilder{ minRefreshInterval: "10s", accessClient: ac, @@ -209,12 +237,13 @@ func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, dashboardService: &dashsvc.DashboardServiceImpl{}, // for validation helpers only folderClientProvider: folderClientProvider, resourcePermissionsSvc: resourcePermissionsSvc, + search: search, isStandalone: true, } } func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion { - if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts, featuremgmt.FlagKubernetesDashboardsV2) { + if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) { // If dashboards v2 is enabled, we want to use v2beta1 as the default API version. return []schema.GroupVersion{ dashv2beta1.DashboardResourceInfo.GroupVersion(), @@ -678,9 +707,10 @@ func (b *DashboardsAPIBuilder) storageForVersion( if err != nil { return err } - storage[dashboards.StoragePath()] = dashboardStoragePermissionWrapper{ - dashboardPermissionsSvc: b.dashboardPermissionsSvc, + storage[dashboards.StoragePath()] = dashboardStorageWrapper{ Storage: dw, + dashboardPermissionsSvc: b.dashboardPermissionsSvc, + live: b.dashboardActivityChannel, } // Register the DTO endpoint that will consolidate all dashboard bits @@ -723,7 +753,6 @@ func (b *DashboardsAPIBuilder) storageForVersion( ResourceInfo: *snapshots, Service: b.snapshotService, Namespacer: b.namespacer, - Options: b.snapshotOptions, } storage[snapshots.StoragePath()] = snapshotLegacyStore storage[snapshots.StoragePath("dashboard")], err = snapshot.NewDashboardREST(dashboards, b.snapshotService) @@ -757,11 +786,6 @@ func (b *DashboardsAPIBuilder) afterDelete(obj runtime.Object, _ *metav1.DeleteO } var defaultDashboardPermissions = []map[string]any{ - { - "kind": "BasicRole", - "name": "Admin", - "verb": "admin", - }, { "kind": "BasicRole", "name": "Editor", diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index e28eeedcecc..08a943b8da6 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -115,6 +115,15 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * Schema: spec.ArrayProperty(spec.StringProperty()), }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "facetLimit", + In: "query", + Description: "maximum number of terms to return per facet (default 50, max 1000)", + Required: false, + Schema: spec.Int64Property(), + }, + }, { ParameterProps: spec3.ParameterProps{ Name: "tags", @@ -340,6 +349,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, user identity.Requester, getDashboardsUIDsSharedWithUser func() ([]string, error)) (*resourcepb.ResourceSearchRequest, error) { // get limit and offset from query params limit := 50 + facetLimit := 50 offset := 0 page := 1 if queryParams.Has("limit") { @@ -422,11 +432,19 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use // The facet term fields if facets, ok := queryParams["facet"]; ok { + if queryParams.Has("facetLimit") { + if parsed, err := strconv.Atoi(queryParams.Get("facetLimit")); err == nil && parsed > 0 { + facetLimit = parsed + if facetLimit > 1000 { + facetLimit = 1000 + } + } + } searchRequest.Facet = make(map[string]*resourcepb.ResourceSearchRequest_Facet) for _, v := range facets { searchRequest.Facet[v] = &resourcepb.ResourceSearchRequest_Facet{ Field: v, - Limit: 50, + Limit: int64(facetLimit), } } } diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index ab7f05e7d00..3b9935f8247 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -818,6 +818,38 @@ func TestConvertHttpSearchRequestToResourceSearchRequest(t *testing.T) { Federated: []*resourcepb.ResourceKey{folderKey}, }, }, + "facet fields with custom limit": { + queryString: "facet=tags&facetLimit=500", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ + "tags": {Field: "tags", Limit: 500}, + }, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "facet fields with limit exceeding max": { + queryString: "facet=tags&facetLimit=5000", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ + "tags": {Field: "tags", Limit: 1000}, + }, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, "tag filter": { queryString: "tag=tag1&tag=tag2", expected: &resourcepb.ResourceSearchRequest{ @@ -1103,3 +1135,7 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) ( func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error { return nil } + +func (m *MockClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, nil +} diff --git a/pkg/registry/apis/dashboard/snapshot/routes.go b/pkg/registry/apis/dashboard/snapshot/routes.go index c8175d6d9dd..832589f5c68 100644 --- a/pkg/registry/apis/dashboard/snapshot/routes.go +++ b/pkg/registry/apis/dashboard/snapshot/routes.go @@ -29,6 +29,8 @@ func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharin createCmd := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateCommand"].Schema createExample := `{"dashboard":{"annotations":{"list":[{"name":"Annotations & Alerts","enable":true,"iconColor":"rgba(0, 211, 255, 1)","snapshotData":[],"type":"dashboard","builtIn":1,"hide":true}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":0,"id":203,"links":[],"liveNow":false,"panels":[{"datasource":null,"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","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","value":null},{"color":"red","value":80}]},"unitScale":true},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":0},"id":1,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"10.4.0-pre","snapshotData":[{"fields":[{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"showPoints":"auto","thresholdsStyle":{"mode":"off"}},"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"time","type":"time","values":[1706030536378,1706034856378,1706039176378,1706043496378,1706047816378,1706052136378]},{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","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","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"A-series","type":"number","values":[1,20,90,30,50,0]}],"refId":"A"}],"targets":[],"title":"Simple example","type":"timeseries","links":[]}],"refresh":"","schemaVersion":39,"snapshot":{"timestamp":"2024-01-23T23:22:16.377Z"},"tags":[],"templating":{"list":[]},"time":{"from":"2024-01-23T17:22:20.380Z","to":"2024-01-23T23:22:20.380Z","raw":{"from":"now-6h","to":"now"}},"timepicker":{},"timezone":"","title":"simple and small","uid":"b22ec8db-399b-403b-b6c7-b0fb30ccb2a5","version":1,"weekStart":""},"name":"simple and small","expires":86400}` createRsp := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateResponse"].Schema + getSettingsRsp := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.SnapshotSharingOptions"].Schema + getSettingsRspExample := `{"snapshotsEnabled":true,"externalSnapshotURL":"https://externalurl.com","externalSnapshotName":"external","externalEnabled":true}` return &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ @@ -167,5 +169,84 @@ func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharin }) }, }, + { + Path: prefix + "/settings", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]any{ + "x-grafana-action": "get", + "x-kubernetes-group-version-kind": metav1.GroupVersionKind{ + Group: dashv0.GROUP, + Version: dashv0.VERSION, + Kind: "SnapshotSharingOptions", + }, + }, + }, + OperationProps: spec3.OperationProps{ + Tags: tags, + OperationId: "getSnapshotSettings", + Description: "Get Snapshot sharing settings", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &getSettingsRsp, + Example: getSettingsRspExample, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: func(w http.ResponseWriter, r *http.Request) { + user, err := identity.GetRequester(r.Context()) + if err != nil { + errhttp.Write(r.Context(), err, w) + return + } + wrap := &contextmodel.ReqContext{ + Context: &web.Context{ + Req: r, + Resp: web.NewResponseWriter(r.Method, w), + }, + } + + vars := mux.Vars(r) + info, err := authlib.ParseNamespace(vars["namespace"]) + if err != nil { + wrap.JsonApiErr(http.StatusBadRequest, "expected namespace", nil) + return + } + if info.OrgID != user.GetOrgID() { + wrap.JsonApiErr(http.StatusBadRequest, + fmt.Sprintf("user orgId does not match namespace (%d != %d)", info.OrgID, user.GetOrgID()), nil) + return + } + + wrap.JSON(http.StatusOK, options) + }, + }, }} } diff --git a/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go b/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go index aafbc2b283d..7ba2d4228c5 100644 --- a/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go +++ b/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go @@ -2,7 +2,6 @@ package snapshot import ( "context" - "fmt" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +28,6 @@ type SnapshotLegacyStore struct { ResourceInfo utils.ResourceInfo Service dashboardsnapshots.Service Namespacer request.NamespaceMapper - Options dashV0.SnapshotSharingOptions } func (s *SnapshotLegacyStore) New() runtime.Object { @@ -117,15 +115,6 @@ func (s *SnapshotLegacyStore) List(ctx context.Context, options *internalversion } func (s *SnapshotLegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - err = s.checkEnabled(info.Value) - if err != nil { - return nil, err - } query := dashboardsnapshots.GetDashboardSnapshotQuery{ Key: name, } @@ -140,10 +129,3 @@ func (s *SnapshotLegacyStore) Get(ctx context.Context, name string, options *met } return nil, s.ResourceInfo.NewNotFound(name) } - -func (s *SnapshotLegacyStore) checkEnabled(ns string) error { - if !s.Options.SnapshotsEnabled { - return fmt.Errorf("snapshots not enabled") - } - return nil -} diff --git a/pkg/registry/apis/datasource/legacy_store.go b/pkg/registry/apis/datasource/legacy_store.go index aea7ed1daa3..e0526dd2409 100644 --- a/pkg/registry/apis/datasource/legacy_store.go +++ b/pkg/registry/apis/datasource/legacy_store.go @@ -61,20 +61,24 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO } func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - start := time.Now() - defer func() { - metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds()) - }() + if s.dsConfigHandlerRequestsDuration != nil { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds()) + }() + } return s.datasources.GetDataSource(ctx, name) } // Create implements rest.Creater. func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - start := time.Now() - defer func() { - metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) - }() + if s.dsConfigHandlerRequestsDuration != nil { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + } ds, ok := obj.(*v0alpha1.DataSource) if !ok { @@ -85,10 +89,12 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa // Update implements rest.Updater. 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) { - start := time.Now() - defer func() { - metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) - }() + if s.dsConfigHandlerRequestsDuration != nil { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + } old, err := s.Get(ctx, name, &metav1.GetOptions{}) if err != nil { @@ -126,10 +132,12 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up // Delete implements rest.GracefulDeleter. func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - start := time.Now() - defer func() { - metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) - }() + if s.dsConfigHandlerRequestsDuration != nil { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + } err := s.datasources.DeleteDataSource(ctx, name) return nil, false, err diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 92cf07053c7..4b6f27da5c0 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -3,6 +3,7 @@ package datasource import ( "context" "encoding/json" + "errors" "fmt" "maps" @@ -38,14 +39,14 @@ var ( // DataSourceAPIBuilder is used just so wire has something unique to return type DataSourceAPIBuilder struct { datasourceResourceInfo utils.ResourceInfo - - pluginJSON plugins.JSONData - client PluginClient // will only ever be called with the same plugin id! - datasources PluginDatasourceProvider - contextProvider PluginContextWrapper - accessControl accesscontrol.AccessControl - queryTypes *queryV0.QueryTypeDefinitionList - configCrudUseNewApis bool + pluginJSON plugins.JSONData + client PluginClient // will only ever be called with the same plugin id! + datasources PluginDatasourceProvider + contextProvider PluginContextWrapper + accessControl accesscontrol.AccessControl + queryTypes *queryV0.QueryTypeDefinitionList + configCrudUseNewApis bool + dataSourceCRUDMetric *prometheus.HistogramVec } func RegisterAPIService( @@ -66,6 +67,16 @@ func RegisterAPIService( var err error var builder *DataSourceAPIBuilder + dataSourceCRUDMetric := metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}) + regErr := reg.Register(dataSourceCRUDMetric) + if regErr != nil && !errors.As(regErr, &prometheus.AlreadyRegisteredError{}) { + return nil, regErr + } + pluginJSONs, err := getDatasourcePlugins(pluginSources) if err != nil { return nil, fmt.Errorf("error getting list of datasource plugins: %s", err) @@ -91,6 +102,7 @@ func RegisterAPIService( if err != nil { return nil, err } + builder.SetDataSourceCRUDMetrics(dataSourceCRUDMetric) apiRegistrar.RegisterAPI(builder) } @@ -161,6 +173,10 @@ func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion { return b.datasourceResourceInfo.GroupVersion() } +func (b *DataSourceAPIBuilder) SetDataSourceCRUDMetrics(datasourceCRUDMetric *prometheus.HistogramVec) { + b.dataSourceCRUDMetric = datasourceCRUDMetric +} + func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, &datasourceV0.DataSource{}, @@ -218,13 +234,9 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver if b.configCrudUseNewApis { legacyStore := &legacyStorage{ - datasources: b.datasources, - resourceInfo: &ds, - dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "grafana", - Name: "ds_config_handler_requests_duration_seconds", - Help: "Duration of requests handled by datasource configuration handlers", - }, []string{"code_path", "handler"}), + datasources: b.datasources, + resourceInfo: &ds, + dsConfigHandlerRequestsDuration: b.dataSourceCRUDMetric, } unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter) if err != nil { diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 72a4abf8e0a..7c39fbe22ae 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -203,11 +203,6 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API } var defaultPermissions = []map[string]any{ - { - "kind": "BasicRole", - "name": "Admin", - "verb": "admin", - }, { "kind": "BasicRole", "name": "Editor", @@ -361,7 +356,7 @@ func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes, if !ok { return fmt.Errorf("obj is not folders.Folder") } - return validateOnUpdate(ctx, f, old, b.storage, b.parents, folder.MaxNestedFolderDepth) + return validateOnUpdate(ctx, f, old, b.storage, b.parents, b.searcher, folder.MaxNestedFolderDepth) default: return nil } diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index 066f8793776..1eb00f0a7e5 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -376,6 +376,10 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { m.On("Get", mock.Anything, "new-parent", mock.Anything).Return( &folders.Folder{}, nil).Once() + // also retrieves old parent for depth difference calculation + m.On("Get", mock.Anything, "valid-parent", mock.Anything).Return( + &folders.Folder{}, + nil).Once() }, }, { diff --git a/pkg/registry/apis/folders/validate.go b/pkg/registry/apis/folders/validate.go index 4f8ccd2250d..3c7c30a1aea 100644 --- a/pkg/registry/apis/folders/validate.go +++ b/pkg/registry/apis/folders/validate.go @@ -6,6 +6,7 @@ import ( "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apiserver/pkg/registry/rest" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" @@ -13,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util" ) @@ -53,6 +55,7 @@ func validateOnCreate(ctx context.Context, f *folders.Folder, getter parentsGett return folder.ErrFolderCannotBeParentOfItself } + // note: `parents` will include itself as the last item parents, err := getter(ctx, f) if err != nil { return fmt.Errorf("unable to create folder inside parent: %w", err) @@ -72,6 +75,7 @@ func validateOnUpdate(ctx context.Context, old *folders.Folder, getter rest.Getter, parents parentsGetter, + searcher resourcepb.ResourceIndexClient, maxDepth int, ) error { folderObj, err := utils.MetaAccessor(obj) @@ -94,7 +98,10 @@ func validateOnUpdate(ctx context.Context, // Validate the move operation newParent := folderObj.GetFolder() - // If we move to root, we don't need to validate the depth. + // If we move to root, we don't need to validate the depth, because the folder already existed + // before and wasn't too deep. This move will make it more shallow. + // + // We also don't need to validate circular references because the root folder cannot have a parent. if newParent == folder.RootFolderUID { return nil } @@ -112,9 +119,6 @@ func validateOnUpdate(ctx context.Context, if !ok { return fmt.Errorf("expected folder, found %T", parentObj) } - - //FIXME: until we have a way to represent the tree, we can only - // look at folder parents to check how deep the new folder tree will be info, err := parents(ctx, parent) if err != nil { return err @@ -128,13 +132,162 @@ func validateOnUpdate(ctx context.Context, } } - // if by moving a folder we exceed the max depth, return an error + // if by moving a folder we exceed the max depth just from its parents + itself, return an error if len(info.Items) > maxDepth+1 { return folder.ErrMaximumDepthReached.Errorf("maximum folder depth reached") } + // To try to save some computation, get the parents of the old parent (this is typically cheaper + // than looking at the children of the folder). If the old parent has more parents or the same + // number of parents as the new parent, we can return early, because we know the folder had to be + // safe from the creation validation. If we cannot access the older parent, we will continue to check the children. + if canSkipChildrenCheck(ctx, oldFolder, getter, parents, len(info.Items)) { + return nil + } + + // Now comes the more expensive part: we need to check if moving this folder will cause + // any descendant folders to exceed the max depth. + // + // Calculate the maximum allowed subtree depth after the move. + allowedDepth := (maxDepth + 1) - len(info.Items) + if allowedDepth <= 0 { + return nil + } + + return checkSubtreeDepth(ctx, searcher, obj.Namespace, obj.Name, allowedDepth, maxDepth) +} + +// canSkipChildrenCheck determines if we can skip the expensive children depth check. +// If the old parent depth is >= the new parent depth, the folder was already valid +// and this move won't make descendants exceed max depth. +func canSkipChildrenCheck(ctx context.Context, oldFolder utils.GrafanaMetaAccessor, getter rest.Getter, parents parentsGetter, newParentDepth int) bool { + if oldFolder.GetFolder() == folder.RootFolderUID { + return false + } + + oldParentObj, err := getter.Get(ctx, oldFolder.GetFolder(), &metav1.GetOptions{}) + if err != nil { + return false + } + + oldParent, ok := oldParentObj.(*folders.Folder) + if !ok { + return false + } + + oldInfo, err := parents(ctx, oldParent) + if err != nil { + return false + } + + oldParentDepth := len(oldInfo.Items) + levelDifference := newParentDepth - oldParentDepth + return levelDifference <= 0 +} + +// checkSubtreeDepth uses a hybrid DFS+batching approach: +// 1. fetches one page of children for the current folder(s) +// 2. batches all those children into one request to get their children +// 3. continues depth-first (batching still) until max depth or violation +// 4. only fetches more siblings after fully exploring current batch +func checkSubtreeDepth(ctx context.Context, searcher resourcepb.ResourceIndexClient, namespace string, folderUID string, remainingDepth int, maxDepth int) error { + if remainingDepth <= 0 { + return nil + } + + // Start with the folder being moved + return checkSubtreeDepthBatched(ctx, searcher, namespace, []string{folderUID}, remainingDepth, maxDepth) +} + +// checkSubtreeDepthBatched checks depth for a batch of folders at the same level +func checkSubtreeDepthBatched(ctx context.Context, searcher resourcepb.ResourceIndexClient, namespace string, parentUIDs []string, remainingDepth int, maxDepth int) error { + if remainingDepth <= 0 || len(parentUIDs) == 0 { + return nil + } + + const pageSize int64 = 1000 + var offset int64 + totalPages := 0 + hasMore := true + + // Using an upper limit to ensure no infinite loops can happen + for hasMore && totalPages < 1000 { + totalPages++ + + var err error + var children []string + children, hasMore, err = getChildrenBatch(ctx, searcher, namespace, parentUIDs, pageSize, offset) + if err != nil { + return fmt.Errorf("failed to get children: %w", err) + } + + if len(children) == 0 { + return nil + } + + // if we are at the last allowed depth and children exist, we will hit the max + if remainingDepth == 1 { + return folder.ErrMaximumDepthReached.Errorf("maximum folder depth %d would be exceeded after move", maxDepth) + } + + if err := checkSubtreeDepthBatched(ctx, searcher, namespace, children, remainingDepth-1, maxDepth); err != nil { + return err + } + + if !hasMore { + return nil + } + + offset += pageSize + } + return nil } +// getChildrenBatch fetches children for multiple parents +func getChildrenBatch(ctx context.Context, searcher resourcepb.ResourceIndexClient, namespace string, parentUIDs []string, limit int64, offset int64) ([]string, bool, error) { + if len(parentUIDs) == 0 { + return nil, false, nil + } + + resp, err := searcher.Search(ctx, &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Namespace: namespace, + Group: folders.FolderResourceInfo.GroupVersionResource().Group, + Resource: folders.FolderResourceInfo.GroupVersionResource().Resource, + }, + Fields: []*resourcepb.Requirement{{ + Key: resource.SEARCH_FIELD_FOLDER, + Operator: string(selection.In), + Values: parentUIDs, + }}, + }, + Limit: limit, + Offset: offset, + }) + if err != nil { + return nil, false, fmt.Errorf("failed to search folders: %w", err) + } + + if resp.Error != nil { + return nil, false, fmt.Errorf("search error: %s", resp.Error.Message) + } + + if resp.Results == nil || len(resp.Results.Rows) == 0 { + return nil, false, nil + } + + children := make([]string, 0, len(resp.Results.Rows)) + for _, row := range resp.Results.Rows { + if row.Key != nil { + children = append(children, row.Key.Name) + } + } + + hasMore := resp.Results.NextPageToken != "" + return children, hasMore, nil +} + func validateOnDelete(ctx context.Context, f *folders.Folder, searcher resourcepb.ResourceIndexClient, diff --git a/pkg/registry/apis/folders/validate_test.go b/pkg/registry/apis/folders/validate_test.go index c4bf07bc71c..b9f55571ce0 100644 --- a/pkg/registry/apis/folders/validate_test.go +++ b/pkg/registry/apis/folders/validate_test.go @@ -20,8 +20,7 @@ func TestValidateCreate(t *testing.T) { tests := []struct { name string folder *folders.Folder - getter *folders.FolderInfoList - getterError error + mockFolders map[string]*folders.Folder expectedErr string maxDepth int // defaults to 5 unless set }{ @@ -36,10 +35,23 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "p2", Parent: "p3"}, - {Name: "p3"}, + mockFolders: map[string]*folders.Folder{ + "p2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p2", + Annotations: map[string]string{"grafana.app/folder": "p3"}, + }, + Spec: folders.FolderSpec{ + Title: "p2 title", + }, + }, + "p3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p3", + }, + Spec: folders.FolderSpec{ + Title: "p3 title", + }, }, }, }, @@ -94,12 +106,41 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "p2", Parent: "p3"}, - {Name: "p3", Parent: "p4"}, - {Name: "p4", Parent: folder.GeneralFolderUID}, - {Name: folder.GeneralFolderUID}, + mockFolders: map[string]*folders.Folder{ + "p2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p2", + Annotations: map[string]string{"grafana.app/folder": "p3"}, + }, + Spec: folders.FolderSpec{ + Title: "p2 title", + }, + }, + "p3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p3", + Annotations: map[string]string{"grafana.app/folder": "p4"}, + }, + Spec: folders.FolderSpec{ + Title: "p3 title", + }, + }, + "p4": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p4", + Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID}, + }, + Spec: folders.FolderSpec{ + Title: "p4 title", + }, + }, + folder.GeneralFolderUID: { + ObjectMeta: metav1.ObjectMeta{ + Name: folder.GeneralFolderUID, + }, + Spec: folders.FolderSpec{ + Title: "General", + }, }, }, maxDepth: 2, @@ -116,17 +157,95 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "4", Parent: "3"}, - {Name: "3", Parent: "2"}, - {Name: "2", Parent: "1"}, - {Name: "1", Parent: folder.GeneralFolderUID}, - {Name: folder.GeneralFolderUID}, + mockFolders: map[string]*folders.Folder{ + "4": { + ObjectMeta: metav1.ObjectMeta{ + Name: "4", + Annotations: map[string]string{"grafana.app/folder": "3"}, + }, + Spec: folders.FolderSpec{ + Title: "4 title", + }, + }, + "3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": "2"}, + }, + Spec: folders.FolderSpec{ + Title: "3 title", + }, + }, + "2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "2", + Annotations: map[string]string{"grafana.app/folder": "1"}, + }, + Spec: folders.FolderSpec{ + Title: "2 title", + }, + }, + "1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "1", + }, + Spec: folders.FolderSpec{ + Title: "1 title", + }, }, }, maxDepth: folder.MaxNestedFolderDepth, }, + { + name: "cannot create a circular reference", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": "2"}, + }, + Spec: folders.FolderSpec{ + Title: "some title", + }, + }, + expectedErr: "cyclic folder references found", + mockFolders: map[string]*folders.Folder{ + "2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "2", + Annotations: map[string]string{"grafana.app/folder": "1"}, + }, + Spec: folders.FolderSpec{ + Title: "2 title", + }, + }, + "1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "1", + Annotations: map[string]string{"grafana.app/folder": "3"}, + }, + Spec: folders.FolderSpec{ + Title: "1 title", + }, + }, + "3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID}, + }, + Spec: folders.FolderSpec{ + Title: "3 title", + }, + }, + folder.GeneralFolderUID: { + ObjectMeta: metav1.ObjectMeta{ + Name: folder.GeneralFolderUID, + }, + Spec: folders.FolderSpec{ + Title: "General", + }, + }, + }, + }, } for _, tt := range tests { @@ -135,10 +254,16 @@ func TestValidateCreate(t *testing.T) { if maxDepth == 0 { maxDepth = 5 } - err := validateOnCreate(context.Background(), tt.folder, - func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) { - return tt.getter, tt.getterError - }, maxDepth) + + mockStorage := grafanarest.NewMockStorage(t) + for name, f := range tt.mockFolders { + f.Name = name + mockStorage.On("Get", context.Background(), name, &metav1.GetOptions{}).Return(f, nil).Maybe() + } + + getter := newParentsGetter(mockStorage, maxDepth) + + err := validateOnCreate(context.Background(), tt.folder, getter, maxDepth) if tt.expectedErr == "" { require.NoError(t, err) @@ -157,6 +282,7 @@ func TestValidateUpdate(t *testing.T) { old *folders.Folder parents *folders.FolderInfoList parentsError error + allFolders []folders.Folder expectedErr string maxDepth int // defaults to 5 unless set }{ @@ -329,6 +455,74 @@ func TestValidateUpdate(t *testing.T) { }, expectedErr: "cannot move folder under its own descendant", }, + { + name: "error when moving folder from root to level2 with children exceeds max depth", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "folderWithChildren", + Annotations: map[string]string{ + utils.AnnoKeyFolder: "level2", + }, + }, + Spec: folders.FolderSpec{ + Title: "folder with children", + }, + }, + old: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "folderWithChildren", + }, + Spec: folders.FolderSpec{ + Title: "folder with children", + }, + }, + parents: &folders.FolderInfoList{ + Items: []folders.FolderInfo{ + {Name: "level2", Parent: "level1"}, + {Name: "level1", Parent: folder.GeneralFolderUID}, + {Name: folder.GeneralFolderUID}, + }, + }, + allFolders: []folders.Folder{ + {ObjectMeta: metav1.ObjectMeta{Name: "child1", Annotations: map[string]string{utils.AnnoKeyFolder: "folderWithChildren"}}}, + {ObjectMeta: metav1.ObjectMeta{Name: "grandchild1", Annotations: map[string]string{utils.AnnoKeyFolder: "child1"}}}, + }, + maxDepth: 4, + expectedErr: "[folder.maximum-depth-reached]", + }, + { + name: "can move folder from root level to level1 with children when within max depth", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "folderWithChildren", + Annotations: map[string]string{ + utils.AnnoKeyFolder: "level1", + }, + }, + Spec: folders.FolderSpec{ + Title: "folder with children", + }, + }, + old: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "folderWithChildren", + }, + Spec: folders.FolderSpec{ + Title: "folder with children", + }, + }, + parents: &folders.FolderInfoList{ + Items: []folders.FolderInfo{ + {Name: "level1", Parent: folder.GeneralFolderUID}, + {Name: folder.GeneralFolderUID}, + }, + }, + allFolders: []folders.Folder{ + {ObjectMeta: metav1.ObjectMeta{Name: "child1", Annotations: map[string]string{utils.AnnoKeyFolder: "folderWithChildren"}}}, + {ObjectMeta: metav1.ObjectMeta{Name: "grandchild1", Annotations: map[string]string{utils.AnnoKeyFolder: "child1"}}}, + }, + maxDepth: 4, + }, } for _, tt := range tests { @@ -349,11 +543,17 @@ func TestValidateUpdate(t *testing.T) { }, nil).Maybe() } } + for i := range tt.allFolders { + f := tt.allFolders[i] + m.On("Get", context.Background(), f.Name, &metav1.GetOptions{}).Return(&f, nil).Maybe() + } err := validateOnUpdate(context.Background(), tt.folder, tt.old, m, func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) { return tt.parents, tt.parentsError - }, maxDepth) + }, + &mockSearchClient{folders: tt.allFolders}, + maxDepth) if tt.expectedErr == "" { require.NoError(t, err) @@ -568,8 +768,7 @@ type mockSearchClient struct { stats *resourcepb.ResourceStatsResponse statsErr error - search *resourcepb.ResourceSearchResponse - searchErr error + folders []folders.Folder } // GetStats implements resourcepb.ResourceIndexClient. @@ -578,8 +777,37 @@ func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.Resource } // Search implements resourcepb.ResourceIndexClient. -func (m *mockSearchClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { - return m.search, m.searchErr +func (m *mockSearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + // get the list of parents from the search request + parentSet := make(map[string]bool) + if req.Options != nil && req.Options.Fields != nil { + for _, field := range req.Options.Fields { + if field.Key == "folder" && field.Operator == "in" { + for _, v := range field.Values { + parentSet[v] = true + } + } + } + } + + // find children that match the parent filter + var rows []*resourcepb.ResourceTableRow + for i := range m.folders { + meta, err := utils.MetaAccessor(&m.folders[i]) + if err != nil { + continue + } + parentUID := meta.GetFolder() + if parentSet[parentUID] { + rows = append(rows, &resourcepb.ResourceTableRow{ + Key: &resourcepb.ResourceKey{Name: m.folders[i].Name}, + }) + } + } + + return &resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{Rows: rows}, + }, nil } // RebuildIndexes implements resourcepb.ResourceIndexClient. diff --git a/pkg/registry/apis/iam/api_installer.go b/pkg/registry/apis/iam/api_installer.go new file mode 100644 index 00000000000..1d456dd3777 --- /dev/null +++ b/pkg/registry/apis/iam/api_installer.go @@ -0,0 +1,79 @@ +package iam + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/server" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" + "github.com/grafana/grafana/pkg/services/apiserver/builder" +) + +// ApiInstaller is a generic interface for installing API resources. +// This allows different implementations (OSS noop, Enterprise with features) +// to provide their own resource registration and validation logic. +type ApiInstaller[T runtime.Object] interface { + // GetAuthorizer returns the authorizer for the API group. + GetAuthorizer() authorizer.Authorizer + + // RegisterStorage registers storage and hooks with the API group. + RegisterStorage(apiGroupInfo *server.APIGroupInfo, opts *builder.APIGroupOptions, storage map[string]rest.Storage) error + + // ValidateOnCreate validates object creation. + ValidateOnCreate(ctx context.Context, obj T) error + + // ValidateOnUpdate validates object updates. + ValidateOnUpdate(ctx context.Context, oldObj, newObj T) error + + // ValidateOnDelete validates object deletion. + ValidateOnDelete(ctx context.Context, obj T) error +} + +// RoleApiInstaller provides Role-specific API registration and validation. +// This interface allows enterprise implementations to provide custom role handling +// while keeping the core IAM registration logic in OSS. +type RoleApiInstaller ApiInstaller[*iamv0.Role] + +// NoopApiInstaller is a no-op implementation for when roles are not available (OSS). +type NoopApiInstaller[T runtime.Object] struct { + ResourceInfo utils.ResourceInfo +} + +func (n *NoopApiInstaller[T]) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + return authorizer.DecisionDeny, "Unavailable functionality", nil + }) +} + +func (n *NoopApiInstaller[T]) RegisterStorage(apiGroupInfo *server.APIGroupInfo, opts *builder.APIGroupOptions, storage map[string]rest.Storage) error { + storage[n.ResourceInfo.StoragePath()] = &noopstorage.NoopREST{ResourceInfo: n.ResourceInfo} + return nil +} + +func (n *NoopApiInstaller[T]) ValidateOnCreate(ctx context.Context, obj T) error { + // No validation needed in OSS + return nil +} + +func (n *NoopApiInstaller[T]) ValidateOnUpdate(ctx context.Context, oldObj, newObj T) error { + // No validation needed in OSS + return nil +} + +func (n *NoopApiInstaller[T]) ValidateOnDelete(ctx context.Context, obj T) error { + // No validation needed in OSS + return nil +} + +// ProvideNoopRoleApiInstaller provides a no-op role installer specifically for Role types. +// This is needed for Wire dependency injection which doesn't handle generic functions well. +func ProvideNoopRoleApiInstaller() RoleApiInstaller { + return &NoopApiInstaller[*iamv0.Role]{ + ResourceInfo: iamv0.RoleInfo, + } +} diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index a3997638e1e..8abc67bf885 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -19,9 +19,14 @@ type iamAuthorizer struct { resourceAuthorizer map[string]authorizer.Authorizer // Map resource to its authorizer } -func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient authlib.AccessClient) authorizer.Authorizer { +func newIAMAuthorizer( + accessClient authlib.AccessClient, + legacyAccessClient authlib.AccessClient, + roleApiInstaller RoleApiInstaller, +) authorizer.Authorizer { resourceAuthorizer := make(map[string]authorizer.Authorizer) + serviceAuthorizer := gfauthorizer.NewServiceAuthorizer() // Authorizer that allows any authenticated user // To be used when authorization is handled at the storage layer allowAuthorizer := authorizer.AuthorizerFunc(func( @@ -43,15 +48,14 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth // Access specific resources authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = iamauthorizer.NewCoreRoleAuthorizer(accessClient) - resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.RoleInfo.GetName()] = roleApiInstaller.GetAuthorizer() resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled by the backend wrapper 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 - - serviceAuthorizer := gfauthorizer.NewServiceAuthorizer() + resourceAuthorizer["searchUsers"] = serviceAuthorizer resourceAuthorizer["searchTeams"] = serviceAuthorizer return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer} diff --git a/pkg/registry/apis/iam/authorizer/parent_provider.go b/pkg/registry/apis/iam/authorizer/parent_provider.go new file mode 100644 index 00000000000..4b7555d85ba --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/parent_provider.go @@ -0,0 +1,164 @@ +package authorizer + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + + "github.com/grafana/authlib/authn" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + + 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/apps/provisioning/pkg/auth" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var ( + ErrNoConfigProvider = errors.New("no config provider for group resource") + ErrNoVersionInfo = errors.New("no version info for group resource") + + Versions = map[schema.GroupResource]string{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: folderv1.VERSION, + {Group: dashboardv1.GROUP, Resource: dashboardv1.DASHBOARD_RESOURCE}: dashboardv1.VERSION, + } +) + +// ConfigProvider is a function that provides a rest.Config for a given context. +type ConfigProvider func(ctx context.Context) (*rest.Config, error) + +// DynamicClientFactory is a function that creates a dynamic.Interface from a rest.Config. +// This can be overridden in tests. +type DynamicClientFactory func(config *rest.Config) (dynamic.Interface, error) + +// ParentProvider implementation that fetches the parent folder information from remote API servers. +type ParentProviderImpl struct { + configProviders map[schema.GroupResource]ConfigProvider + versions map[schema.GroupResource]string + dynamicClientFactory DynamicClientFactory + + // Cache of dynamic clients for each group resource + // This is used to avoid creating a new dynamic client for each request + // and to reuse the same client for the same group resource. + clients map[schema.GroupResource]dynamic.Interface + clientsMu sync.Mutex +} + +// DialConfig holds the configuration for dialing a remote API server. +type DialConfig struct { + Host string + Insecure bool + CAFile string + Audience string +} + +// NewLocalConfigProvider creates a map of ConfigProviders that return the same given config for local API servers. +func NewLocalConfigProvider( + configProvider ConfigProvider, +) map[schema.GroupResource]ConfigProvider { + return map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider, + {Group: dashboardv1.GROUP, Resource: dashboardv1.DASHBOARD_RESOURCE}: configProvider, + } +} + +// NewRemoteConfigProvider creates a map of ConfigProviders for remote API servers based on the given DialConfig. +func NewRemoteConfigProvider(cfg map[schema.GroupResource]DialConfig, exchangeClient authn.TokenExchanger) map[schema.GroupResource]ConfigProvider { + configProviders := make(map[schema.GroupResource]ConfigProvider, len(cfg)) + for gr, dialConfig := range cfg { + configProviders[gr] = func(ctx context.Context) (*rest.Config, error) { + return &rest.Config{ + Host: dialConfig.Host, + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { + return auth.NewRoundTripper(exchangeClient, rt, dialConfig.Audience) + }, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: dialConfig.Insecure, + CAFile: dialConfig.CAFile, + }, + QPS: 50, + Burst: 100, + }, nil + } + } + return configProviders +} + +// NewApiParentProvider creates a new ParentProviderImpl with the given config providers and version info. +func NewApiParentProvider( + configProviders map[schema.GroupResource]ConfigProvider, + version map[schema.GroupResource]string, +) *ParentProviderImpl { + return &ParentProviderImpl{ + configProviders: configProviders, + versions: version, + dynamicClientFactory: func(config *rest.Config) (dynamic.Interface, error) { + return dynamic.NewForConfig(config) + }, + clients: make(map[schema.GroupResource]dynamic.Interface), + } +} + +func (p *ParentProviderImpl) HasParent(gr schema.GroupResource) bool { + _, ok := p.configProviders[gr] + return ok +} + +func (p *ParentProviderImpl) getClient(ctx context.Context, gr schema.GroupResource) (dynamic.Interface, error) { + p.clientsMu.Lock() + client, ok := p.clients[gr] + p.clientsMu.Unlock() + + if ok { + return client, nil + } + + provider, ok := p.configProviders[gr] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrNoConfigProvider, gr.String()) + } + restConfig, err := provider(ctx) + if err != nil { + return nil, err + } + + client, err = p.dynamicClientFactory(restConfig) + if err != nil { + return nil, err + } + + p.clientsMu.Lock() + p.clients[gr] = client + p.clientsMu.Unlock() + + return client, nil +} + +func (p *ParentProviderImpl) GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) { + client, err := p.getClient(ctx, gr) + if err != nil { + return "", err + } + + version, ok := p.versions[gr] + if !ok { + return "", fmt.Errorf("%w: %s", ErrNoVersionInfo, gr.String()) + } + resourceClient := client.Resource(schema.GroupVersionResource{ + Group: gr.Group, + Resource: gr.Resource, + Version: version, + }).Namespace(namespace) + + unstructObj, err := resourceClient.Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return "", err + } + + return unstructObj.GetAnnotations()[utils.AnnoKeyFolder], nil +} diff --git a/pkg/registry/apis/iam/authorizer/parent_provider_test.go b/pkg/registry/apis/iam/authorizer/parent_provider_test.go new file mode 100644 index 00000000000..45405d81161 --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/parent_provider_test.go @@ -0,0 +1,198 @@ +package authorizer + +import ( + "context" + "errors" + "testing" + + "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" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + + folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var configProvider = func(ctx context.Context) (*rest.Config, error) { + return &rest.Config{}, nil +} + +func TestParentProviderImpl_GetParent(t *testing.T) { + tests := []struct { + name string + gr schema.GroupResource + namespace string + resourceName string + parentFolder string + setupFake func(*fakeDynamicClient, *fakeResourceInterface) + configProviders map[schema.GroupResource]ConfigProvider + versions map[schema.GroupResource]string + expectedError string + expectedParent string + }{ + { + name: "successfully get parent folder", + gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}, + namespace: "org-1", + resourceName: "dash1", + parentFolder: "fold1", + setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) { + fakeClient.resourceInterface = fakeResource + fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetAnnotations(map[string]string{utils.AnnoKeyFolder: "fold1"}) + return obj, nil + } + }, + configProviders: map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider, + }, + versions: Versions, + expectedParent: "fold1", + }, + { + name: "resource without parent annotation returns empty", + gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}, + namespace: "org-1", + resourceName: "dash1", + setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) { + fakeClient.resourceInterface = fakeResource + fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetAnnotations(map[string]string{}) + return obj, nil + } + }, + configProviders: map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: configProvider, + }, + versions: Versions, + expectedParent: "", + }, + { + name: "no config provider returns error", + gr: schema.GroupResource{Group: "unknown.group", Resource: "unknown"}, + namespace: "org-1", + resourceName: "resource-1", + configProviders: map[schema.GroupResource]ConfigProvider{}, + versions: Versions, + expectedError: ErrNoConfigProvider.Error(), + }, + { + name: "config provider returns error", + gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}, + namespace: "org-1", + resourceName: "resource-1", + configProviders: map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) { + return nil, errors.New("config provider error") + }, + }, + versions: Versions, + expectedError: "config provider error", + }, + { + name: "no version info returns error", + gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}, + namespace: "org-1", + resourceName: "resource-1", + configProviders: map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) { + return &rest.Config{}, nil + }, + }, + versions: map[schema.GroupResource]string{}, + expectedError: ErrNoVersionInfo.Error(), + }, + { + name: "resource get returns error", + gr: schema.GroupResource{Group: folderv1.GROUP, Resource: folderv1.RESOURCE}, + namespace: "org-1", + resourceName: "resource-1", + setupFake: func(fakeClient *fakeDynamicClient, fakeResource *fakeResourceInterface) { + fakeClient.resourceInterface = fakeResource + fakeResource.getFunc = func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + return nil, errors.New("resource not found") + } + }, + configProviders: map[schema.GroupResource]ConfigProvider{ + {Group: folderv1.GROUP, Resource: folderv1.RESOURCE}: func(ctx context.Context) (*rest.Config, error) { + return &rest.Config{}, nil + }, + }, + versions: Versions, + expectedError: "resource not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := &fakeDynamicClient{} + fakeResource := &fakeResourceInterface{} + if tt.setupFake != nil { + tt.setupFake(fakeClient, fakeResource) + } + + provider := &ParentProviderImpl{ + configProviders: tt.configProviders, + versions: tt.versions, + dynamicClientFactory: func(config *rest.Config) (dynamic.Interface, error) { + return fakeClient, nil + }, + clients: make(map[schema.GroupResource]dynamic.Interface), + } + + parent, err := provider.GetParent(context.Background(), tt.gr, tt.namespace, tt.resourceName) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Empty(t, parent) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedParent, parent) + } + }) + } +} + +// fakeDynamicClient is a fake implementation of dynamic.Interface +type fakeDynamicClient struct { + resourceInterface dynamic.ResourceInterface +} + +func (f *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { + return &fakeNamespaceableResourceInterface{ + resourceInterface: f.resourceInterface, + } +} + +// fakeNamespaceableResourceInterface is a fake implementation of dynamic.NamespaceableResourceInterface +type fakeNamespaceableResourceInterface struct { + dynamic.NamespaceableResourceInterface + resourceInterface dynamic.ResourceInterface +} + +func (f *fakeNamespaceableResourceInterface) Namespace(namespace string) dynamic.ResourceInterface { + if f.resourceInterface != nil { + return f.resourceInterface + } + return &fakeResourceInterface{} +} + +// fakeResourceInterface is a fake implementation of dynamic.ResourceInterface +type fakeResourceInterface struct { + dynamic.ResourceInterface + getFunc func(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) +} + +func (f *fakeResourceInterface) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + if f.getFunc != nil { + return f.getFunc(ctx, name, opts, subresources...) + } + return &unstructured.Unstructured{}, nil +} diff --git a/pkg/registry/apis/iam/authorizer/resource_permissions.go b/pkg/registry/apis/iam/authorizer/resource_permissions.go index d857c50bc84..0fbf413adac 100644 --- a/pkg/registry/apis/iam/authorizer/resource_permissions.go +++ b/pkg/registry/apis/iam/authorizer/resource_permissions.go @@ -10,24 +10,44 @@ import ( iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" ) // TODO: Logs, Metrics, Traces? +// ParentProvider interface for fetching parent information of resources +type ParentProvider interface { + // HasParent checks if the given GroupResource has a parent folder + HasParent(gr schema.GroupResource) bool + // GetParent fetches the parent folder name for the given resource + GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) +} + // ResourcePermissionsAuthorizer type ResourcePermissionsAuthorizer struct { - accessClient types.AccessClient + accessClient types.AccessClient + parentProvider ParentProvider + logger log.Logger } var _ storewrapper.ResourceStorageAuthorizer = (*ResourcePermissionsAuthorizer)(nil) -func NewResourcePermissionsAuthorizer(accessClient types.AccessClient) *ResourcePermissionsAuthorizer { +func NewResourcePermissionsAuthorizer( + accessClient types.AccessClient, + parentProvider ParentProvider, +) *ResourcePermissionsAuthorizer { return &ResourcePermissionsAuthorizer{ - accessClient: accessClient, + accessClient: accessClient, + parentProvider: parentProvider, + logger: log.New("iam.authorizer.resource-permissions"), } } +func isAccessPolicy(authInfo types.AuthInfo) bool { + return types.IsIdentityType(authInfo.GetIdentityType(), types.TypeAccessPolicy) +} + // AfterGet implements ResourceStorageAuthorizer. func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error { authInfo, ok := types.AuthInfoFrom(ctx) @@ -37,9 +57,24 @@ func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtim switch o := obj.(type) { case *iamv0.ResourcePermission: target := o.Spec.Resource + targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource} - // TODO: Fetch the resource to retrieve its parent folder. parent := "" + // Fetch the parent of the resource + // Access Policies have global scope, so no parent check needed + if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) { + p, err := r.parentProvider.GetParent(ctx, targetGR, o.Namespace, target.Name) + if err != nil { + r.logger.Error("after get: error fetching parent", "error", err.Error(), + "namespace", o.Namespace, + "group", target.ApiGroup, + "resource", target.Resource, + "name", target.Name, + ) + return err + } + parent = p + } checkReq := types.CheckRequest{ Namespace: o.Namespace, @@ -72,9 +107,24 @@ func (r *ResourcePermissionsAuthorizer) beforeWrite(ctx context.Context, obj run switch o := obj.(type) { case *iamv0.ResourcePermission: target := o.Spec.Resource + targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource} - // TODO: Fetch the resource to retrieve its parent folder. parent := "" + // Fetch the parent of the resource + // Access Policies have global scope, so no parent check needed + if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) { + p, err := r.parentProvider.GetParent(ctx, targetGR, o.Namespace, target.Name) + if err != nil { + r.logger.Error("before write: error fetching parent", "error", err.Error(), + "namespace", o.Namespace, + "group", target.ApiGroup, + "resource", target.Resource, + "name", target.Name, + ) + return err + } + parent = p + } checkReq := types.CheckRequest{ Namespace: o.Namespace, @@ -153,8 +203,28 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run canViewFuncs[gr] = canView } - // TODO : Fetch the resource to retrieve its parent folder. + target := item.Spec.Resource + targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource} + parent := "" + // Fetch the parent of the resource + // It's not efficient to do for every item in the list, but it's a good starting point. + // Access Policies have global scope, so no parent check needed + if !isAccessPolicy(authInfo) && r.parentProvider.HasParent(targetGR) { + p, err := r.parentProvider.GetParent(ctx, targetGR, item.Namespace, target.Name) + if err != nil { + // Skip item on error fetching parent + r.logger.Warn("filter list: error fetching parent, skipping item", + "error", err.Error(), + "namespace", item.Namespace, + "group", target.ApiGroup, + "resource", target.Resource, + "name", target.Name, + ) + continue + } + parent = p + } allowed := canView(item.Spec.Resource.Name, parent) if allowed { diff --git a/pkg/registry/apis/iam/authorizer/resource_permissions_test.go b/pkg/registry/apis/iam/authorizer/resource_permissions_test.go index 9f1762e365f..df777ee31e9 100644 --- a/pkg/registry/apis/iam/authorizer/resource_permissions_test.go +++ b/pkg/registry/apis/iam/authorizer/resource_permissions_test.go @@ -5,13 +5,15 @@ import ( "testing" "github.com/go-jose/go-jose/v4/jwt" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( @@ -63,6 +65,7 @@ func TestResourcePermissions_AfterGet(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + parent := "fold-1" checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { require.NotNil(t, id) // Check is called with the user's identity @@ -74,12 +77,18 @@ func TestResourcePermissions_AfterGet(t *testing.T) { require.Equal(t, fold1.Spec.Resource.Resource, req.Resource) require.Equal(t, fold1.Spec.Resource.Name, req.Name) require.Equal(t, utils.VerbGetPermissions, req.Verb) + require.Equal(t, parent, folder) return types.CheckResponse{Allowed: tt.shouldAllow}, nil } + getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) { + // For this test, we can return a fixed parent folder ID + return parent, nil + } accessClient := &fakeAccessClient{checkFunc: checkFunc} - resPermAuthz := NewResourcePermissionsAuthorizer(accessClient) + fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc} + resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider) ctx := types.WithAuthInfo(context.Background(), user) err := resPermAuthz.AfterGet(ctx, fold1) @@ -89,6 +98,7 @@ func TestResourcePermissions_AfterGet(t *testing.T) { require.Error(t, err, "expected error for denied access") } require.True(t, accessClient.checkCalled, "accessClient.Check should be called") + require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called") }) } } @@ -121,23 +131,32 @@ func TestResourcePermissions_FilterList(t *testing.T) { require.Equal(t, "dashboards", req.Resource) } - // Return a checker that allows only specific resources: fold-1 and dash-2 + // Return a checker that allows access to fold-1 and its content return func(name, folder string) bool { - if name == "fold-1" || name == "dash-2" { + if name == "fold-1" || folder == "fold-1" { return true } return false }, &types.NoopZookie{}, nil } + getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) { + if name == "dash-2" { + return "fold-1", nil + } + return "", nil + } + accessClient := &fakeAccessClient{compileFunc: compileFunc} - resPermAuthz := NewResourcePermissionsAuthorizer(accessClient) + fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc} + resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider) ctx := types.WithAuthInfo(context.Background(), user) obj, err := resPermAuthz.FilterList(ctx, list) require.NoError(t, err) require.NotNil(t, list) require.True(t, accessClient.compileCalled, "accessClient.Compile should be called") + require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called") filtered, ok := obj.(*iamv0.ResourcePermissionList) require.True(t, ok, "response should be of type ResourcePermissionList") @@ -165,6 +184,7 @@ func TestResourcePermissions_beforeWrite(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + parent := "fold-1" checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { require.NotNil(t, id) // Check is called with the user's identity @@ -176,12 +196,18 @@ func TestResourcePermissions_beforeWrite(t *testing.T) { require.Equal(t, fold1.Spec.Resource.Resource, req.Resource) require.Equal(t, fold1.Spec.Resource.Name, req.Name) require.Equal(t, utils.VerbSetPermissions, req.Verb) + require.Equal(t, parent, folder) return types.CheckResponse{Allowed: tt.shouldAllow}, nil } + getParentFunc := func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) { + return parent, nil + } + accessClient := &fakeAccessClient{checkFunc: checkFunc} - resPermAuthz := NewResourcePermissionsAuthorizer(accessClient) + fakeParentProvider := &fakeParentProvider{hasParent: true, getParentFunc: getParentFunc} + resPermAuthz := NewResourcePermissionsAuthorizer(accessClient, fakeParentProvider) ctx := types.WithAuthInfo(context.Background(), user) err := resPermAuthz.beforeWrite(ctx, fold1) @@ -191,6 +217,7 @@ func TestResourcePermissions_beforeWrite(t *testing.T) { require.Error(t, err, "expected error for denied delete") } require.True(t, accessClient.checkCalled, "accessClient.Check should be called") + require.True(t, fakeParentProvider.getParentCalled, "parentProvider.GetParent should be called") }) } } @@ -214,3 +241,18 @@ func (m *fakeAccessClient) Compile(ctx context.Context, id types.AuthInfo, req t } var _ types.AccessClient = (*fakeAccessClient)(nil) + +type fakeParentProvider struct { + hasParent bool + getParentCalled bool + getParentFunc func(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) +} + +func (f *fakeParentProvider) HasParent(gr schema.GroupResource) bool { + return f.hasParent +} + +func (f *fakeParentProvider) GetParent(ctx context.Context, gr schema.GroupResource, namespace, name string) (string, error) { + f.getParentCalled = true + return f.getParentFunc(ctx, gr, namespace, name) +} diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index b851122ffd9..9e4d8e484e4 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" + iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer" "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" @@ -55,11 +56,15 @@ type IdentityAccessManagementAPIBuilder struct { teamBindingLegacyStore *teambinding.LegacyBindingStore ssoLegacyStore *sso.LegacyStore coreRolesStorage CoreRoleStorageBackend - rolesStorage RoleStorageBackend + roleApiInstaller RoleApiInstaller resourcePermissionsStorage resource.StorageBackend roleBindingsStorage RoleBindingStorageBackend externalGroupMappingStorage ExternalGroupMappingStorageBackend + // Required for resource permissions authorization + // fetches resources parent folders + resourceParentProvider iamauthorizer.ParentProvider + // Access Control authorizer authorizer.Authorizer // legacyAccessClient is used for the identity apis, we need to migrate to the access client @@ -77,10 +82,11 @@ type IdentityAccessManagementAPIBuilder struct { reg prometheus.Registerer logger log.Logger - dual dualwrite.Service - unified resource.ResourceClient - userSearchClient resourcepb.ResourceIndexClient - teamSearch *TeamSearchHandler + dual dualwrite.Service + unified resource.ResourceClient + userSearchClient resourcepb.ResourceIndexClient + userSearchHandler *user.SearchHandler + teamSearch *TeamSearchHandler teamGroupsHandler externalgroupmapping.TeamGroupsHandler diff --git a/pkg/registry/apis/iam/noopstorage/rest.go b/pkg/registry/apis/iam/noopstorage/rest.go new file mode 100644 index 00000000000..aa0214788ab --- /dev/null +++ b/pkg/registry/apis/iam/noopstorage/rest.go @@ -0,0 +1,74 @@ +package noopstorage + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +type NoopREST struct { + ResourceInfo utils.ResourceInfo +} + +var ( + _ rest.Storage = (*NoopREST)(nil) + _ rest.Scoper = (*NoopREST)(nil) + _ rest.SingularNameProvider = (*NoopREST)(nil) + _ rest.Getter = (*NoopREST)(nil) +) + +func (n *NoopREST) New() runtime.Object { + return n.ResourceInfo.NewFunc() +} + +func (n *NoopREST) NewList() runtime.Object { + return n.ResourceInfo.NewListFunc() +} + +func (n *NoopREST) NamespaceScoped() bool { + return true +} + +func (n *NoopREST) GetSingularName() string { + return n.ResourceInfo.GetSingularName() +} + +func (n *NoopREST) Destroy() {} + +func (n *NoopREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return nil, errNoopStorage +} + +func (n *NoopREST) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + return nil, errNoopStorage +} + +func (n *NoopREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + return nil, errNoopStorage +} + +func (n *NoopREST) 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, errNoopStorage +} + +func (n *NoopREST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + return nil, false, errNoopStorage +} + +func (n *NoopREST) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { + return nil, errNoopStorage +} + +func (n *NoopREST) Watch(ctx context.Context, options *internalversion.ListOptions) (watch.Interface, error) { + return nil, errNoopStorage +} + +func (n *NoopREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return nil, errNoopStorage +} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index b9da9e6f2c5..8a3dcb9586b 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -21,6 +21,7 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" @@ -41,11 +42,13 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/teambinding" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver" gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" teamservice "github.com/grafana/grafana/pkg/services/team" legacyuser "github.com/grafana/grafana/pkg/services/user" @@ -69,39 +72,47 @@ func RegisterAPIService( zClient zanzana.Client, reg prometheus.Registerer, coreRolesStorage CoreRoleStorageBackend, - rolesStorage RoleStorageBackend, + roleApiInstaller RoleApiInstaller, tracing *tracing.TracingService, roleBindingsStorage RoleBindingStorageBackend, externalGroupMappingStorageBackend ExternalGroupMappingStorageBackend, teamGroupsHandlerImpl externalgroupmapping.TeamGroupsHandler, dual dualwrite.Service, unified resource.ResourceClient, + orgService org.Service, userService legacyuser.Service, teamService teamservice.Service, + restConfig apiserver.RestConfigProvider, ) (*IdentityAccessManagementAPIBuilder, error) { dbProvider := legacysql.NewDatabaseProvider(sql) store := legacy.NewLegacySQLStores(dbProvider) legacyAccessClient := newLegacyAccessClient(ac, store) - authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) + authorizer := newIAMAuthorizer(accessClient, legacyAccessClient, roleApiInstaller) registerMetrics(reg) //nolint:staticcheck // not yet migrated to OpenFeature enableAuthnMutation := features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + resourceParentProvider := iamauthorizer.NewApiParentProvider( + iamauthorizer.NewLocalConfigProvider(restConfig.GetRestConfig), + iamauthorizer.Versions, + ) + builder := &IdentityAccessManagementAPIBuilder{ store: store, - userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), - saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), - legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), - teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), - ssoLegacyStore: sso.NewLegacyStore(ssoService), + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation, tracing), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing), + ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing), coreRolesStorage: coreRolesStorage, - rolesStorage: rolesStorage, + roleApiInstaller: roleApiInstaller, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), roleBindingsStorage: roleBindingsStorage, externalGroupMappingStorage: externalGroupMappingStorageBackend, teamGroupsHandler: teamGroupsHandlerImpl, sso: ssoService, + resourceParentProvider: resourceParentProvider, authorizer: authorizer, legacyAccessClient: legacyAccessClient, accessClient: accessClient, @@ -114,9 +125,11 @@ func RegisterAPIService( dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), - unified, user.NewUserLegacySearchClient(userService), features), + unified, user.NewUserLegacySearchClient(orgService, tracing, cfg), features), teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features), } + builder.userSearchHandler = user.NewSearchHandler(tracing, builder.userSearchClient, features, cfg) + apiregistration.RegisterAPI(builder) return builder, nil @@ -126,10 +139,12 @@ func NewAPIService( accessClient types.AccessClient, dbProvider legacysql.LegacyDatabaseProvider, coreRoleStorage CoreRoleStorageBackend, - roleStorage RoleStorageBackend, + roleApiInstaller RoleApiInstaller, features featuremgmt.FeatureToggles, zClient zanzana.Client, reg prometheus.Registerer, + tokenExchanger authn.TokenExchanger, + authorizerDialConfigs map[schema.GroupResource]iamauthorizer.DialConfig, ) *IdentityAccessManagementAPIBuilder { store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) @@ -138,19 +153,25 @@ func NewAPIService( resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) coreRoleAuthorizer := iamauthorizer.NewCoreRoleAuthorizer(accessClient) + resourceParentProvider := iamauthorizer.NewApiParentProvider( + iamauthorizer.NewRemoteConfigProvider(authorizerDialConfigs, tokenExchanger), + iamauthorizer.Versions, + ) + return &IdentityAccessManagementAPIBuilder{ store: store, display: user.NewLegacyDisplayREST(store), resourcePermissionsStorage: resourcePermissionsStorage, - rolesStorage: roleStorage, coreRolesStorage: coreRoleStorage, roleBindingsStorage: noopstorage.ProvideStorageBackend(), // TODO: add a proper storage backend logger: log.New("iam.apis"), features: features, accessClient: accessClient, + resourceParentProvider: resourceParentProvider, zClient: zClient, zTickets: make(chan bool, MaxConcurrentZanzanaWrites), reg: reg, + roleApiInstaller: roleApiInstaller, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { user, ok := types.AuthInfoFrom(ctx) @@ -356,23 +377,17 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } if enableZanzanaSync { b.logger.Info("Enabling hooks for CoreRole to sync to Zanzana") - coreRoleStore.AfterCreate = b.AfterRoleCreate - coreRoleStore.AfterDelete = b.AfterRoleDelete - coreRoleStore.BeginUpdate = b.BeginRoleUpdate + h := NewRoleHooks(b.zClient, b.zTickets, b.logger) + coreRoleStore.AfterCreate = h.AfterRoleCreate + coreRoleStore.AfterDelete = h.AfterRoleDelete + coreRoleStore.BeginUpdate = h.BeginRoleUpdate } storage[iamv0.CoreRoleInfo.StoragePath()] = coreRoleStore - roleStore, err := NewLocalStore(iamv0.RoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.rolesStorage) - if err != nil { + // Role registration is delegated to the RoleApiInstaller + if err := b.roleApiInstaller.RegisterStorage(apiGroupInfo, &opts, storage); err != nil { return err } - if enableZanzanaSync { - 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 roleBindingStore, err := NewLocalStore(iamv0.RoleBindingInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.roleBindingsStorage) if err != nil { @@ -440,7 +455,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( return fmt.Errorf("expected RegistryStoreDualWrite, got %T", dw) } - authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient)) + authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient, b.resourceParentProvider)) storage[iamv0.ResourcePermissionInfo.StoragePath()] = authzWrapper return nil @@ -510,10 +525,18 @@ func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenA func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) - routes := b.teamSearch.GetAPIRoutes(defs) - routes.Namespace = append(routes.Namespace, b.display.GetAPIRoutes(defs).Namespace...) + searchRoutes := make([]*builder.APIRoutes, 0, 2) + if b.userSearchHandler != nil { + searchRoutes = append(searchRoutes, b.userSearchHandler.GetAPIRoutes(defs)) + } - return routes + if b.teamSearch != nil { + searchRoutes = append(searchRoutes, b.teamSearch.GetAPIRoutes(defs)) + } + + routes := []*builder.APIRoutes{b.display.GetAPIRoutes(defs)} + routes = append(routes, searchRoutes...) + return mergeAPIRoutes(routes...) } func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer { @@ -539,6 +562,8 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return resourcepermission.ValidateCreateAndUpdateInput(ctx, typedObj) case *iamv0.ExternalGroupMapping: return externalgroupmapping.ValidateOnCreate(typedObj) + case *iamv0.Role: + return b.roleApiInstaller.ValidateOnCreate(ctx, typedObj) } return nil case admission.Update: @@ -563,9 +588,19 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return fmt.Errorf("expected old object to be a TeamBinding, got %T", oldTeamBindingObj) } return teambinding.ValidateOnUpdate(ctx, typedObj, oldTeamBindingObj) + case *iamv0.Role: + oldRoleObj, ok := a.GetOldObject().(*iamv0.Role) + if !ok { + return fmt.Errorf("expected old object to be a Role, got %T", oldRoleObj) + } + return b.roleApiInstaller.ValidateOnUpdate(ctx, oldRoleObj, typedObj) } return nil case admission.Delete: + switch oldRoleObj := a.GetOldObject().(type) { + case *iamv0.Role: + return b.roleApiInstaller.ValidateOnDelete(ctx, oldRoleObj) + } return nil case admission.Connect: return nil @@ -621,3 +656,15 @@ func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defa store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) return store, err } + +func mergeAPIRoutes(routes ...*builder.APIRoutes) *builder.APIRoutes { + merged := &builder.APIRoutes{} + for _, r := range routes { + if r == nil { + continue + } + merged.Root = append(merged.Root, r.Root...) + merged.Namespace = append(merged.Namespace, r.Namespace...) + } + return merged +} diff --git a/pkg/registry/apis/iam/role_hooks.go b/pkg/registry/apis/iam/role_hooks.go index 831a2993fe5..8d86a35bd27 100644 --- a/pkg/registry/apis/iam/role_hooks.go +++ b/pkg/registry/apis/iam/role_hooks.go @@ -9,12 +9,23 @@ import ( "k8s.io/apiserver/pkg/registry/generic/registry" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" "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" ) +type RoleHooks struct { + zClient zanzana.Client + zTickets chan bool + logger log.Logger +} + +func NewRoleHooks(zClient zanzana.Client, zTickets chan bool, logger log.Logger) *RoleHooks { + return &RoleHooks{zClient: zClient, zTickets: zTickets, logger: logger} +} + // 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) { @@ -44,8 +55,8 @@ func convertRolePermissionsToTuples(roleUID string, permissions []iamv0.CoreRole // 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 { +func (h *RoleHooks) AfterRoleCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if h.zClient == nil { return } @@ -76,21 +87,21 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, } wait := time.Now() - b.zTickets <- true + h.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 + <-h.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", + h.logger.Error("failed to convert role permissions to tuples", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -103,7 +114,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, // Avoid writing if there are no valid tuples if len(tuples) == 0 { - b.logger.Debug("no valid tuples to write for role", + h.logger.Debug("no valid tuples to write for role", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -113,7 +124,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, return } - b.logger.Debug("writing role permissions to zanzana", + h.logger.Debug("writing role permissions to zanzana", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -124,14 +135,14 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) defer cancel() - err = b.zClient.Write(ctx, &v1.WriteRequest{ + err = h.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", + h.logger.Error("failed to write role permissions to zanzana", "err", err, "namespace", role.Namespace, "roleUID", role.Name, @@ -149,8 +160,8 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, // 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 { +func (h *RoleHooks) AfterRoleDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if h.zClient == nil { return } @@ -182,15 +193,15 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, } wait := time.Now() - b.zTickets <- true + h.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 + <-h.zTickets }() - b.logger.Debug("deleting role permissions from zanzana", + h.logger.Debug("deleting role permissions from zanzana", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -199,7 +210,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, tuples, err := convertRolePermissionsToTuples(role.Name, role.Spec.Permissions) if err != nil { - b.logger.Error("failed to convert role permissions to tuples for deletion", + h.logger.Error("failed to convert role permissions to tuples for deletion", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -211,7 +222,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, // Avoid deleting if there are no valid tuples if len(tuples) == 0 { - b.logger.Debug("no valid tuples to delete for role", + h.logger.Debug("no valid tuples to delete for role", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -223,7 +234,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, // Convert tuples to TupleKeyWithoutCondition for deletion deleteTuples := toTupleKeysWithoutCondition(tuples) - b.logger.Debug("deleting role permissions from zanzana", + h.logger.Debug("deleting role permissions from zanzana", "namespace", role.Namespace, "roleUID", role.Name, "roleType", roleType, @@ -234,14 +245,14 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) defer cancel() - err = b.zClient.Write(ctx, &v1.WriteRequest{ + err = h.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", + h.logger.Error("failed to delete role permissions from zanzana", "err", err, "namespace", role.Namespace, "roleUID", role.Name, @@ -255,8 +266,8 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, // 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 { +func (h *RoleHooks) BeginRoleUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if h.zClient == nil { return nil, nil } var oldRole, newRole *iamv0.CoreRole @@ -316,12 +327,12 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context // Grab a ticket to write to Zanzana wait := time.Now() - b.zTickets <- true + h.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 + <-h.zTickets }() roleUID, namespace := old.Name, old.Namespace oldPermissions, newPermissions := old.Spec.Permissions, new.Spec.Permissions @@ -332,7 +343,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context var err error oldTuples, err = convertRolePermissionsToTuples(roleUID, oldPermissions) if err != nil { - b.logger.Error("failed to convert old role permissions to tuples", + h.logger.Error("failed to convert old role permissions to tuples", "namespace", namespace, "roleUID", roleUID, "roleType", roleType, @@ -344,7 +355,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context // 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", + h.logger.Error("failed to convert new role permissions to tuples", "namespace", namespace, "roleUID", roleUID, "roleType", roleType, @@ -353,7 +364,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context return } - b.logger.Debug("updating role permissions in zanzana", + h.logger.Debug("updating role permissions in zanzana", "namespace", namespace, "roleUID", roleUID, "roleType", roleType, @@ -375,7 +386,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context req.Deletes = &v1.WriteRequestDeletes{ TupleKeys: deleteTuples, } - b.logger.Debug("deleting existing role permissions from zanzana", + h.logger.Debug("deleting existing role permissions from zanzana", "namespace", namespace, "roleUID", roleUID, "roleType", roleType, @@ -388,7 +399,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context req.Writes = &v1.WriteRequestWrites{ TupleKeys: newTuples, } - b.logger.Debug("writing new role permissions to zanzana", + h.logger.Debug("writing new role permissions to zanzana", "namespace", namespace, "roleUID", roleUID, "roleType", roleType, @@ -398,9 +409,9 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context // Only make the request if there are deletes or writes if req.Deletes != nil || req.Writes != nil { - err = b.zClient.Write(ctx, req) + err = h.zClient.Write(ctx, req) if err != nil { - b.logger.Error("failed to update role permissions in zanzana", + h.logger.Error("failed to update role permissions in zanzana", "err", err, "namespace", namespace, "roleUID", roleUID, diff --git a/pkg/registry/apis/iam/role_hooks_test.go b/pkg/registry/apis/iam/role_hooks_test.go index 497150f1c61..847d0c7727a 100644 --- a/pkg/registry/apis/iam/role_hooks_test.go +++ b/pkg/registry/apis/iam/role_hooks_test.go @@ -53,7 +53,7 @@ func requireDeleteTuplesMatch(t *testing.T, actual []*v1.TupleKeyWithoutConditio func TestAfterCoreRoleCreate(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -90,8 +90,8 @@ func TestAfterCoreRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleEntries} - b.AfterRoleCreate(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleEntries} + h.AfterRoleCreate(&coreRole, nil) wg.Wait() }) @@ -129,8 +129,8 @@ func TestAfterCoreRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleEntries} - b.AfterRoleCreate(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleEntries} + h.AfterRoleCreate(&coreRole, nil) wg.Wait() }) @@ -163,8 +163,8 @@ func TestAfterCoreRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testWildcardEntries} - b.AfterRoleCreate(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testWildcardEntries} + h.AfterRoleCreate(&coreRole, nil) wg.Wait() }) @@ -198,15 +198,15 @@ func TestAfterCoreRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMixedEntries} - b.AfterRoleCreate(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testMixedEntries} + h.AfterRoleCreate(&coreRole, nil) wg.Wait() }) } func TestAfterRoleCreate(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -243,8 +243,8 @@ func TestAfterRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testRoleEntries} - b.AfterRoleCreate(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testRoleEntries} + h.AfterRoleCreate(&role, nil) wg.Wait() }) @@ -281,8 +281,8 @@ func TestAfterRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleEntries} - b.AfterRoleCreate(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testDashRoleEntries} + h.AfterRoleCreate(&role, nil) wg.Wait() }) @@ -317,15 +317,15 @@ func TestAfterRoleCreate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMergedEntries} - b.AfterRoleCreate(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testMergedEntries} + h.AfterRoleCreate(&role, nil) wg.Wait() }) } func TestBeginCoreRoleUpdate(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -387,10 +387,10 @@ func TestBeginCoreRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testUpdate} + h.zClient = &FakeZanzanaClient{writeCallback: testUpdate} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -445,10 +445,10 @@ func TestBeginCoreRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testExpand} + h.zClient = &FakeZanzanaClient{writeCallback: testExpand} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -499,10 +499,10 @@ func TestBeginCoreRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testClear} + h.zClient = &FakeZanzanaClient{writeCallback: testClear} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -514,7 +514,7 @@ func TestBeginCoreRoleUpdate(t *testing.T) { func TestBeginRoleUpdate(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -577,10 +577,10 @@ func TestBeginRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testUpdate} + h.zClient = &FakeZanzanaClient{writeCallback: testUpdate} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -640,10 +640,10 @@ func TestBeginRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testSwap} + h.zClient = &FakeZanzanaClient{writeCallback: testSwap} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -695,10 +695,10 @@ func TestBeginRoleUpdate(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testAddToEmpty} + h.zClient = &FakeZanzanaClient{writeCallback: testAddToEmpty} // Call BeginUpdate which does all the work - finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + finishFunc, err := h.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) require.NoError(t, err) require.NotNil(t, finishFunc) @@ -710,7 +710,7 @@ func TestBeginRoleUpdate(t *testing.T) { func TestAfterCoreRoleDelete(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -747,8 +747,8 @@ func TestAfterCoreRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleDeletes} - b.AfterRoleDelete(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleDeletes} + h.AfterRoleDelete(&coreRole, nil) wg.Wait() }) @@ -785,8 +785,8 @@ func TestAfterCoreRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleDeletes} - b.AfterRoleDelete(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleDeletes} + h.AfterRoleDelete(&coreRole, nil) wg.Wait() }) @@ -818,8 +818,8 @@ func TestAfterCoreRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testWildcardDeletes} - b.AfterRoleDelete(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testWildcardDeletes} + h.AfterRoleDelete(&coreRole, nil) wg.Wait() }) @@ -853,15 +853,15 @@ func TestAfterCoreRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMixedDeletes} - b.AfterRoleDelete(&coreRole, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testMixedDeletes} + h.AfterRoleDelete(&coreRole, nil) wg.Wait() }) } func TestAfterRoleDelete(t *testing.T) { var wg sync.WaitGroup - b := &IdentityAccessManagementAPIBuilder{ + h := &RoleHooks{ logger: log.NewNopLogger(), zTickets: make(chan bool, 1), } @@ -898,8 +898,8 @@ func TestAfterRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testRoleDeletes} - b.AfterRoleDelete(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testRoleDeletes} + h.AfterRoleDelete(&role, nil) wg.Wait() }) @@ -936,8 +936,8 @@ func TestAfterRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleDeletes} - b.AfterRoleDelete(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testDashRoleDeletes} + h.AfterRoleDelete(&role, nil) wg.Wait() }) @@ -982,8 +982,8 @@ func TestAfterRoleDelete(t *testing.T) { return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMultiDeletes} - b.AfterRoleDelete(&role, nil) + h.zClient = &FakeZanzanaClient{writeCallback: testMultiDeletes} + h.AfterRoleDelete(&role, nil) wg.Wait() }) } diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 2980322cb27..db4709a4e2e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // DeleteCollection implements rest.CollectionDeleter. @@ -52,6 +54,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -95,6 +100,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } @@ -165,6 +173,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) { @@ -228,6 +239,9 @@ func extractPluginNameFromTitle(title string) string { } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/sso/store.go b/pkg/registry/apis/iam/sso/store.go index c9965adc152..9ab09fa6ef4 100644 --- a/pkg/registry/apis/iam/sso/store.go +++ b/pkg/registry/apis/iam/sso/store.go @@ -7,6 +7,7 @@ import ( commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,12 +33,13 @@ var ( var resource = iamv0.SSOSettingResourceInfo -func NewLegacyStore(service ssosettings.Service) *LegacyStore { - return &LegacyStore{service} +func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{service, tracer} } type LegacyStore struct { service ssosettings.Service + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -71,6 +73,9 @@ func (s *LegacyStore) NewList() runtime.Object { // List implements rest.Lister. func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.List") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) settings, err := s.service.List(ctx) @@ -88,6 +93,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt // Get implements rest.Getter. func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.Get") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name) @@ -112,6 +120,9 @@ func (s *LegacyStore) Update( _ bool, _ *metav1.UpdateOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Update") + defer span.End() + const created = false ident, err := identity.GetRequester(ctx) if err != nil { @@ -148,6 +159,9 @@ func (s *LegacyStore) Delete( _ rest.ValidateObjectFunc, options *metav1.DeleteOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Delete") + defer span.End() + obj, err := s.Get(ctx, name, nil) if err != nil { return obj, false, err diff --git a/pkg/registry/apis/iam/team/legacy_search.go b/pkg/registry/apis/iam/team/legacy_search.go index 938062bd9e6..8f9594b96ac 100644 --- a/pkg/registry/apis/iam/team/legacy_search.go +++ b/pkg/registry/apis/iam/team/legacy_search.go @@ -6,6 +6,7 @@ import ( "log/slog" "math" "strconv" + "strings" "google.golang.org/grpc" @@ -102,7 +103,8 @@ func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition { columns := getDefaultColumns() for _, field := range fields { - if col, ok := builders.TeamSearchTableColumnDefinitions[field]; ok { + fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX) + if col, ok := builders.TeamSearchTableColumnDefinitions[fieldName]; ok { columns = append(columns, col) } } @@ -121,7 +123,8 @@ func getDefaultColumns() []*resourcepb.ResourceTableColumnDefinition { func createCells(t *team.TeamDTO, fields []string) [][]byte { cells := createDefaultCells(t) for _, field := range fields { - switch field { + fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX) + switch fieldName { case builders.TEAM_SEARCH_EMAIL: cells = append(cells, []byte(t.Email)) case builders.TEAM_SEARCH_PROVISIONED: diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index e048fdb7a6d..c667c8ec374 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.TeamResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } func (s *LegacyStore) New() runtime.Object { @@ -74,6 +76,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -112,6 +117,9 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -161,6 +169,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) { @@ -197,6 +208,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -219,6 +233,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/registry/apis/iam/team_search.go b/pkg/registry/apis/iam/team_search.go index 786bf9be834..2252263a708 100644 --- a/pkg/registry/apis/iam/team_search.go +++ b/pkg/registry/apis/iam/team_search.go @@ -2,6 +2,7 @@ package iam import ( "encoding/json" + "fmt" "net/http" "net/url" "strconv" @@ -12,6 +13,7 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -137,6 +139,12 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request) return } + requester, err := identity.GetRequester(ctx) + if err != nil { + errhttp.Write(ctx, fmt.Errorf("no identity found for request: %w", err), w) + return + } + limit := 50 offset := 0 page := 1 @@ -154,16 +162,23 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request) } searchRequest := &resourcepb.ResourceSearchRequest{ - Options: &resourcepb.ListOptions{}, + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Group: iamv0alpha1.TeamResourceInfo.GroupResource().Group, + Resource: iamv0alpha1.TeamResourceInfo.GroupResource().Resource, + Namespace: requester.GetNamespace(), + }, + }, Query: queryParams.Get("query"), Limit: int64(limit), Offset: int64(offset), Page: int64(page), Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false", Fields: []string{ - builders.TEAM_SEARCH_EMAIL, - builders.TEAM_SEARCH_PROVISIONED, - builders.TEAM_SEARCH_EXTERNAL_UID, + resource.SEARCH_FIELD_TITLE, + resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EMAIL, + resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PROVISIONED, + resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EXTERNAL_UID, }, } diff --git a/pkg/registry/apis/iam/team_search_test.go b/pkg/registry/apis/iam/team_search_test.go index ccc1abbf18c..2142ba4e505 100644 --- a/pkg/registry/apis/iam/team_search_test.go +++ b/pkg/registry/apis/iam/team_search_test.go @@ -89,7 +89,7 @@ func TestTeamSearchHandler(t *testing.T) { if mockClient.LastSearchRequest == nil { t.Fatalf("expected Search to be called, but it was not") } - expectedFields := []string{"email", "provisioned", "externalUID"} + expectedFields := []string{"title", "fields.email", "fields.provisioned", "fields.externalUID"} if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } @@ -284,3 +284,6 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) ( func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error { return nil } +func (m *MockClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, nil +} diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index eaaa4766e39..6f6fd4ce162 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,13 +36,14 @@ var ( _ rest.CollectionDeleter = (*LegacyBindingStore)(nil) ) -func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool) *LegacyBindingStore { - return &LegacyBindingStore{store, enableAuthnMutation} +func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool, tracer trace.Tracer) *LegacyBindingStore { + return &LegacyBindingStore{store, enableAuthnMutation, tracer} } type LegacyBindingStore struct { store legacy.LegacyIdentityStore enableAuthnMutation bool + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -73,6 +75,9 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime. } func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Update") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") } @@ -125,6 +130,9 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re } func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Delete") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete") } @@ -160,6 +168,9 @@ func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidat } func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Create") + defer span.End() + if !l.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "create") } @@ -230,6 +241,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre // Get implements rest.Getter. func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -254,6 +268,9 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta // List implements rest.Lister. func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.List") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 534a4f016eb..41b54995c38 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -2,15 +2,22 @@ package user import ( "context" + "encoding/binary" "fmt" "log/slog" "math" + "regexp" + "sort" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/user" - res "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/searchusers/sortopts" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) @@ -20,47 +27,69 @@ const ( UserResourceGroup = "iam.grafana.com" ) -var _ resourcepb.ResourceIndexClient = (*UserLegacySearchClient)(nil) +var ( + _ resourcepb.ResourceIndexClient = (*UserLegacySearchClient)(nil) + fieldLogin = fmt.Sprintf("%s%s", resource.SEARCH_FIELD_PREFIX, builders.USER_LOGIN) + fieldEmail = fmt.Sprintf("%s%s", resource.SEARCH_FIELD_PREFIX, builders.USER_EMAIL) + fieldLastSeenAt = fmt.Sprintf("%s%s", resource.SEARCH_FIELD_PREFIX, builders.USER_LAST_SEEN_AT) + fieldRole = fmt.Sprintf("%s%s", resource.SEARCH_FIELD_PREFIX, builders.USER_ROLE) + wildcardsMatcher = regexp.MustCompile(`[\*\?\\]`) +) // UserLegacySearchClient is a client for searching for users in the legacy search engine. type UserLegacySearchClient struct { resourcepb.ResourceIndexClient - userService user.Service - log *slog.Logger + orgService org.Service + log *slog.Logger + tracer trace.Tracer + cfg *setting.Cfg } // NewUserLegacySearchClient creates a new UserLegacySearchClient. -func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient { +func NewUserLegacySearchClient(orgService org.Service, tracer trace.Tracer, cfg *setting.Cfg) *UserLegacySearchClient { return &UserLegacySearchClient{ - userService: userService, - log: slog.Default().With("logger", "legacy-user-search-client"), + orgService: orgService, + log: slog.Default().With("logger", "legacy-user-search-client"), + tracer: tracer, + cfg: cfg, } } // Search searches for users in the legacy search engine. // It only supports exact matching for title, login, or email. -// FIXME: This implementation only supports a single field query and will be extended in the future. func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + ctx, span := c.tracer.Start(ctx, "user.Search") + defer span.End() + signedInUser, err := identity.GetRequester(ctx) if err != nil { return nil, err } - if req.Limit > 100 { - req.Limit = 100 + if req.Limit > maxLimit { + req.Limit = maxLimit } if req.Limit <= 0 { - req.Limit = 1 + req.Limit = 30 } if req.Page > math.MaxInt32 || req.Page < 0 { return nil, fmt.Errorf("invalid page number: %d", req.Page) } - query := &user.SearchUsersQuery{ - SignedInUser: signedInUser, - Limit: int(req.Limit), - Page: int(req.Page), + if req.Page < 1 { + req.Page = 1 + } + + legacySortOptions := convertToSortOptions(req.SortBy) + + query := &org.SearchOrgUsersQuery{ + OrgID: signedInUser.GetOrgID(), + Limit: int(req.Limit), + Page: int(req.Page), + SortOpts: legacySortOptions, + + User: signedInUser, } var title, login, email string @@ -70,19 +99,15 @@ func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.Res c.log.Warn("only single value fields are supported for legacy search, using first value", "field", field.Key, "values", vals) } switch field.Key { - case res.SEARCH_FIELD_TITLE: + case resource.SEARCH_FIELD_TITLE: title = vals[0] - case "fields.login": + case fieldLogin: login = vals[0] - case "fields.email": + case fieldEmail: email = vals[0] } } - if title == "" && login == "" && email == "" { - return nil, fmt.Errorf("at least one of title, login, or email must be provided for the query") - } - // The user store's Search method combines these into an OR. // For legacy search we can only supply one. if title != "" { @@ -93,20 +118,35 @@ func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.Res query.Query = email } - columns := getColumns(req.Fields) + // Unified search `query` has wildcards, but legacy search does not support them. + // We have to remove them here to make legacy search work as expected with SQL LIKE queries. + if req.Query != "" { + query.Query = wildcardsMatcher.ReplaceAllString(req.Query, "") + } + + fields := req.Fields + if len(fields) == 0 { + fields = []string{resource.SEARCH_FIELD_TITLE, fieldEmail, fieldLogin, fieldLastSeenAt, fieldRole} + } + + columns := getColumns(fields) list := &resourcepb.ResourceSearchResponse{ Results: &resourcepb.ResourceTable{ Columns: columns, }, } - res, err := c.userService.Search(ctx, query) + res, err := c.orgService.SearchOrgUsers(ctx, query) if err != nil { return nil, err } - for _, u := range res.Users { - cells := createBaseCells(u, req.Fields) + for _, u := range res.OrgUsers { + if c.isHiddenUser(u.Login, signedInUser) { + continue + } + + cells := createCells(u, req.Fields) list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{ Key: getResourceKey(u, req.Options.Key.Namespace), Cells: cells, @@ -117,7 +157,19 @@ func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.Res return list, nil } -func getResourceKey(item *user.UserSearchHitDTO, namespace string) *resourcepb.ResourceKey { +func (c *UserLegacySearchClient) isHiddenUser(login string, signedInUser identity.Requester) bool { + if login == "" || signedInUser.GetIsGrafanaAdmin() || login == signedInUser.GetUsername() { + return false + } + + if _, hidden := c.cfg.HiddenUsers[login]; hidden { + return true + } + + return false +} + +func getResourceKey(item *org.OrgUserDTO, namespace string) *resourcepb.ResourceKey { return &resourcepb.ResourceKey{ Namespace: namespace, Group: UserResourceGroup, @@ -127,42 +179,74 @@ func getResourceKey(item *user.UserSearchHitDTO, namespace string) *resourcepb.R } func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition { - columns := defaultColumns() + cols := make([]*resourcepb.ResourceTableColumnDefinition, 0, len(fields)) + standardSearchFields := resource.StandardSearchFields() for _, field := range fields { switch field { - case "email": - columns = append(columns, builders.UserTableColumnDefinitions[builders.USER_EMAIL]) - case "login": - columns = append(columns, builders.UserTableColumnDefinitions[builders.USER_LOGIN]) + case resource.SEARCH_FIELD_TITLE: + cols = append(cols, standardSearchFields.Field(resource.SEARCH_FIELD_TITLE)) + case fieldLastSeenAt: + cols = append(cols, builders.UserTableColumnDefinitions[builders.USER_LAST_SEEN_AT]) + case fieldRole: + cols = append(cols, builders.UserTableColumnDefinitions[builders.USER_ROLE]) + case fieldEmail: + cols = append(cols, builders.UserTableColumnDefinitions[builders.USER_EMAIL]) + case fieldLogin: + cols = append(cols, builders.UserTableColumnDefinitions[builders.USER_LOGIN]) } } - return columns + return cols } -func createBaseCells(u *user.UserSearchHitDTO, fields []string) [][]byte { - cells := createDefaultCells(u) +func createCells(u *org.OrgUserDTO, fields []string) [][]byte { + cells := make([][]byte, 0, len(fields)) for _, field := range fields { switch field { - case "email": + case resource.SEARCH_FIELD_TITLE: + cells = append(cells, []byte(u.Name)) + case fieldEmail: cells = append(cells, []byte(u.Email)) - case "login": + case fieldLogin: cells = append(cells, []byte(u.Login)) + case fieldLastSeenAt: + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(u.LastSeenAt.Unix())) + cells = append(cells, b) + case fieldRole: + cells = append(cells, []byte(u.Role)) } } return cells } -func createDefaultCells(u *user.UserSearchHitDTO) [][]byte { - return [][]byte{ - []byte(u.UID), - []byte(u.Name), - } -} +func convertToSortOptions(sortBy []*resourcepb.ResourceSearchRequest_Sort) []model.SortOption { + opts := []model.SortOption{} + for _, s := range sortBy { + field := s.Field + // Handle mapping if necessary + switch field { + case fieldLastSeenAt: + field = "lastSeenAtAge" + case resource.SEARCH_FIELD_TITLE: + field = "name" + case fieldLogin: + field = "login" + case fieldEmail: + field = "email" + } -func defaultColumns() []*resourcepb.ResourceTableColumnDefinition { - searchFields := res.StandardSearchFields() - return []*resourcepb.ResourceTableColumnDefinition{ - searchFields.Field(res.SEARCH_FIELD_NAME), - searchFields.Field(res.SEARCH_FIELD_TITLE), + suffix := "asc" + if s.Desc { + suffix = "desc" + } + key := fmt.Sprintf("%s-%s", field, suffix) + + if opt, ok := sortopts.SortOptionsByQueryParam[key]; ok { + opts = append(opts, opt) + } } + sort.Slice(opts, func(i, j int) bool { + return opts[i].Index < opts[j].Index || (opts[i].Index == opts[j].Index && opts[i].Name < opts[j].Name) + }) + return opts } diff --git a/pkg/registry/apis/iam/user/legacy_search_fake.go b/pkg/registry/apis/iam/user/legacy_search_fake.go index b68e944c84c..e2effd7947e 100644 --- a/pkg/registry/apis/iam/user/legacy_search_fake.go +++ b/pkg/registry/apis/iam/user/legacy_search_fake.go @@ -5,7 +5,7 @@ import ( "google.golang.org/grpc" - "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -13,7 +13,7 @@ import ( type FakeUserLegacySearchClient struct { resourcepb.ResourceIndexClient SearchFunc func(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) - Users []*user.UserSearchHitDTO + Users []*org.OrgUserDTO } // Search calls the underlying SearchFunc or simulates a search over the Users slice. @@ -23,7 +23,7 @@ func (c *FakeUserLegacySearchClient) Search(ctx context.Context, req *resourcepb } // Basic filtering for testing purposes - var filteredUsers []*user.UserSearchHitDTO + var filteredUsers []*org.OrgUserDTO var queryValue string for _, field := range req.Options.Fields { @@ -43,7 +43,7 @@ func (c *FakeUserLegacySearchClient) Search(ctx context.Context, req *resourcepb for _, u := range filteredUsers { rows = append(rows, &resourcepb.ResourceTableRow{ Key: getResourceKey(u, req.Options.Key.Namespace), - Cells: createBaseCells(u, req.Fields), + Cells: createCells(u, req.Fields), }) } diff --git a/pkg/registry/apis/iam/user/legacy_search_test.go b/pkg/registry/apis/iam/user/legacy_search_test.go index b8a754f34ce..e3dc873fe3c 100644 --- a/pkg/registry/apis/iam/user/legacy_search_test.go +++ b/pkg/registry/apis/iam/user/legacy_search_test.go @@ -8,29 +8,16 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/grafana/grafana/pkg/setting" res "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) func TestUserLegacySearchClient_Search(t *testing.T) { - t.Run("should return error if no query fields are provided", func(t *testing.T) { - mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) - req := &resourcepb.ResourceSearchRequest{ - Options: &resourcepb.ListOptions{ - Key: &resourcepb.ResourceKey{Namespace: "default"}, - }, - } - - _, err := client.Search(ctx, req) - - require.Error(t, err) - require.Equal(t, "at least one of title, login, or email must be provided for the query", err.Error()) - }) - testCases := []struct { name string fieldKey string @@ -65,8 +52,8 @@ func TestUserLegacySearchClient_Search(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + mockOrgService := orgtest.NewMockService(t) + client := NewUserLegacySearchClient(mockOrgService, tracing.NewNoopTracerService(), &setting.Cfg{}) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Limit: 10, @@ -80,14 +67,14 @@ func TestUserLegacySearchClient_Search(t *testing.T) { Fields: []string{"email", "login"}, } - mockUsers := []*user.UserSearchHitDTO{ - {ID: 1, UID: "uid1", Name: "Test User 1", Email: "test1@example.com", Login: "testlogin1"}, + mockUsers := []*org.OrgUserDTO{ + {UID: "uid1", Name: "Test User 1", Email: "test1@example.com", Login: "testlogin1"}, } - mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool { + mockOrgService.On("SearchOrgUsers", mock.Anything, mock.MatchedBy(func(q *org.SearchOrgUsersQuery) bool { return q.Query == tc.expectedQuery && q.Limit == 10 && q.Page == 1 - })).Return(&user.SearchUserQueryResult{ - Users: mockUsers, + })).Return(&org.SearchOrgUsersQueryResult{ + OrgUsers: mockUsers, TotalCount: 1, }, nil) @@ -112,7 +99,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { require.Equal(t, UserResource, row.Key.Resource) require.Equal(t, u.UID, row.Key.Name) - expectedCells := createBaseCells(&user.UserSearchHitDTO{ + expectedCells := createCells(&org.OrgUserDTO{ UID: u.UID, Name: u.Name, Email: u.Email, @@ -124,8 +111,8 @@ func TestUserLegacySearchClient_Search(t *testing.T) { } t.Run("title should have precedence over login and email", func(t *testing.T) { - mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + mockOrgService := orgtest.NewMockService(t) + client := NewUserLegacySearchClient(mockOrgService, tracing.NewNoopTracerService(), &setting.Cfg{}) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ @@ -138,9 +125,9 @@ func TestUserLegacySearchClient_Search(t *testing.T) { }, } - mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool { + mockOrgService.On("SearchOrgUsers", mock.Anything, mock.MatchedBy(func(q *org.SearchOrgUsersQuery) bool { return q.Query == "title" - })).Return(&user.SearchUserQueryResult{Users: []*user.UserSearchHitDTO{}, TotalCount: 0}, nil) + })).Return(&org.SearchOrgUsersQueryResult{OrgUsers: []*org.OrgUserDTO{}, TotalCount: 0}, nil) _, err := client.Search(ctx, req) require.NoError(t, err) diff --git a/pkg/registry/apis/iam/user/search.go b/pkg/registry/apis/iam/user/search.go new file mode 100644 index 00000000000..d2de838105a --- /dev/null +++ b/pkg/registry/apis/iam/user/search.go @@ -0,0 +1,399 @@ +package user + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/url" + "regexp" + "slices" + "strconv" + "strings" + "time" + + "go.opentelemetry.io/otel/trace" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "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/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errhttp" +) + +const maxLimit = 100 + +type SearchHandler struct { + log *slog.Logger + client resourcepb.ResourceIndexClient + tracer trace.Tracer + features featuremgmt.FeatureToggles + cfg *setting.Cfg +} + +func NewSearchHandler(tracer trace.Tracer, searchClient resourcepb.ResourceIndexClient, features featuremgmt.FeatureToggles, cfg *setting.Cfg) *SearchHandler { + return &SearchHandler{ + client: searchClient, + log: slog.Default().With("logger", "grafana-apiserver.user.search"), + tracer: tracer, + features: features, + cfg: cfg, + } +} + +func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes { + searchResults := defs["github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchUsers"].Schema + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "searchUsers", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + Description: "User search", + Tags: []string{"Search"}, + OperationId: "getSearchUsers", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "query", + In: "query", + Required: false, + Schema: spec.StringProperty(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "limit", + In: "query", + Description: "number of results to return", + Example: 30, + Required: false, + Schema: spec.Int64Property(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "page", + In: "query", + Description: "page number (starting from 1)", + Example: 1, + Required: false, + Schema: spec.Int64Property(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "offset", + In: "query", + Description: "number of results to skip", + Example: 0, + Required: false, + Schema: spec.Int64Property(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "sort", + In: "query", + Description: "sortable field", + Example: "", + Examples: map[string]*spec3.Example{ + "": { + ExampleProps: spec3.ExampleProps{ + Summary: "default sorting", + Value: "", + }, + }, + "title": { + ExampleProps: spec3.ExampleProps{ + Summary: "title ascending", + Value: "title", + }, + }, + "-title": { + ExampleProps: spec3.ExampleProps{ + Summary: "title descending", + Value: "-title", + }, + }, + "lastSeenAt": { + ExampleProps: spec3.ExampleProps{ + Summary: "last seen at ascending", + Value: "lastSeenAt", + }, + }, + "-lastSeenAt": { + ExampleProps: spec3.ExampleProps{ + Summary: "last seen at descending", + Value: "-lastSeenAt", + }, + }, + "email": { + ExampleProps: spec3.ExampleProps{ + Summary: "email ascending", + Value: "email", + }, + }, + "-email": { + ExampleProps: spec3.ExampleProps{ + Summary: "email descending", + Value: "-email", + }, + }, + "login": { + ExampleProps: spec3.ExampleProps{ + Summary: "login ascending", + Value: "login", + }, + }, + "-login": { + ExampleProps: spec3.ExampleProps{ + Summary: "login descending", + Value: "-login", + }, + }, + }, + Required: false, + Schema: spec.StringProperty(), + }, + }, + }, + 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: &searchResults, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: s.DoSearch, + }, + }, + } +} + +func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { + ctx, span := s.tracer.Start(r.Context(), "user.search") + defer span.End() + + queryParams, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + requester, err := identity.GetRequester(ctx) + if err != nil { + errhttp.Write(ctx, fmt.Errorf("no identity found for request: %w", err), w) + return + } + + limit := 30 + offset := 0 + page := 1 + if queryParams.Has("limit") { + limit, _ = strconv.Atoi(queryParams.Get("limit")) + } + if queryParams.Has("offset") { + offset, _ = strconv.Atoi(queryParams.Get("offset")) + if offset > 0 && limit > 0 { + page = (offset / limit) + 1 + } + } else if queryParams.Has("page") { + page, _ = strconv.Atoi(queryParams.Get("page")) + offset = (page - 1) * limit + } + + // Escape characters that are used by bleve wildcard search to be literal strings. + rawQuery := escapeBleveQuery(queryParams.Get("query")) + + searchQuery := fmt.Sprintf(`*%s*`, rawQuery) + + userGvr := iamv0.UserResourceInfo.GroupResource() + request := &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Group: userGvr.Group, + Resource: userGvr.Resource, + Namespace: requester.GetNamespace(), + }, + }, + Query: searchQuery, + Fields: []string{resource.SEARCH_FIELD_TITLE, fieldEmail, fieldLogin, fieldLastSeenAt, fieldRole}, + Limit: int64(limit), + Page: int64(page), + Offset: int64(offset), + } + + if !requester.GetIsGrafanaAdmin() { + // FIXME: Use the new config service instead of the legacy one + hiddenUsers := []string{} + for user := range s.cfg.HiddenUsers { + if user != requester.GetUsername() { + hiddenUsers = append(hiddenUsers, user) + } + } + if len(hiddenUsers) > 0 { + request.Options.Fields = append(request.Options.Fields, &resourcepb.Requirement{ + Key: fieldLogin, + Operator: string(selection.NotIn), + Values: hiddenUsers, + }) + } + } + + if queryParams.Has("sort") { + for _, sort := range queryParams["sort"] { + currField := sort + desc := false + if strings.HasPrefix(sort, "-") { + currField = sort[1:] + desc = true + } + if slices.Contains(builders.UserSortableExtraFields, currField) { + sort = resource.SEARCH_FIELD_PREFIX + currField + } else { + sort = currField + } + s := &resourcepb.ResourceSearchRequest_Sort{ + Field: sort, + Desc: desc, + } + request.SortBy = append(request.SortBy, s) + } + } + + resp, err := s.client.Search(ctx, request) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + result, err := ParseResults(resp) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + s.write(w, result) +} + +func (s *SearchHandler) write(w http.ResponseWriter, obj any) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(obj); err != nil { + s.log.Error("failed to encode JSON response", "error", err) + } +} + +func ParseResults(result *resourcepb.ResourceSearchResponse) (*iamv0.GetSearchUsers, error) { + if result == nil { + return iamv0.NewGetSearchUsers(), nil + } else if result.Error != nil { + return iamv0.NewGetSearchUsers(), fmt.Errorf("%d error searching: %s: %s", result.Error.Code, result.Error.Message, result.Error.Details) + } else if result.Results == nil { + return iamv0.NewGetSearchUsers(), nil + } + + titleIDX := -1 + emailIDX := -1 + loginIDX := -1 + lastSeenAtIDX := -1 + roleIDX := -1 + + for i, v := range result.Results.Columns { + switch v.Name { + case resource.SEARCH_FIELD_TITLE: + titleIDX = i + case builders.USER_EMAIL: + emailIDX = i + case builders.USER_LOGIN: + loginIDX = i + case builders.USER_LAST_SEEN_AT: + lastSeenAtIDX = i + case builders.USER_ROLE: + roleIDX = i + } + } + + sr := iamv0.NewGetSearchUsers() + sr.TotalHits = result.TotalHits + sr.QueryCost = result.QueryCost + sr.MaxScore = result.MaxScore + sr.Hits = make([]iamv0.UserHit, 0, len(result.Results.Rows)) + + for _, row := range result.Results.Rows { + if len(row.Cells) != len(result.Results.Columns) { + return iamv0.NewGetSearchUsers(), fmt.Errorf("error parsing user search response: mismatch number of columns and cells") + } + + var login string + if loginIDX >= 0 && row.Cells[loginIDX] != nil { + login = string(row.Cells[loginIDX]) + } + + hit := iamv0.UserHit{ + Name: row.Key.Name, + Login: login, + } + + if titleIDX >= 0 && row.Cells[titleIDX] != nil { + hit.Title = string(row.Cells[titleIDX]) + } + + if emailIDX >= 0 && row.Cells[emailIDX] != nil { + hit.Email = string(row.Cells[emailIDX]) + } + + if roleIDX >= 0 && row.Cells[roleIDX] != nil { + hit.Role = string(row.Cells[roleIDX]) + } + + if lastSeenAtIDX >= 0 && row.Cells[lastSeenAtIDX] != nil { + if len(row.Cells[lastSeenAtIDX]) == 8 { + hit.LastSeenAt = int64(binary.BigEndian.Uint64(row.Cells[lastSeenAtIDX])) + hit.LastSeenAtAge = util.GetAgeString(time.Unix(hit.LastSeenAt, 0)) + } + } + + sr.Hits = append(sr.Hits, hit) + } + + return sr, nil +} + +var bleveEscapeRegex = regexp.MustCompile(`([\\*?])`) + +func escapeBleveQuery(query string) string { + return bleveEscapeRegex.ReplaceAllString(query, `\$1`) +} diff --git a/pkg/registry/apis/iam/user/search_test.go b/pkg/registry/apis/iam/user/search_test.go new file mode 100644 index 00000000000..588ab37da39 --- /dev/null +++ b/pkg/registry/apis/iam/user/search_test.go @@ -0,0 +1,169 @@ +package user + +import ( + "context" + "net/http/httptest" + "testing" + + "google.golang.org/grpc" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" + legacyuser "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +func TestSearchFallback(t *testing.T) { + tests := []struct { + name string + mode rest.DualWriterMode + expectUnified bool + }{ + {name: "should hit legacy search handler on mode 0", mode: rest.Mode0, expectUnified: false}, + {name: "should hit legacy search handler on mode 1", mode: rest.Mode1, expectUnified: false}, + {name: "should hit legacy search handler on mode 2", mode: rest.Mode2, expectUnified: false}, + {name: "should hit unified storage search handler on mode 3", mode: rest.Mode3, expectUnified: true}, + {name: "should hit unified storage search handler on mode 4", mode: rest.Mode4, expectUnified: true}, + {name: "should hit unified storage search handler on mode 5", mode: rest.Mode5, expectUnified: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": {DualWriterMode: tt.mode}, + }, + } + dual := dualwrite.ProvideStaticServiceForTests(cfg) + + searchClient := resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), mockClient, mockLegacyClient, featuremgmt.WithFeatures()) + searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), searchClient, featuremgmt.WithFeatures(), cfg) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/searchUsers", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &legacyuser.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if tt.expectUnified { + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Unified Search to be called, but it was not") + } + } else { + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Legacy Search to be called, but it was not") + } + } + }) + } +} + +// MockClient implements the ResourceIndexClient interface for testing +type MockClient struct { + resourcepb.ResourceIndexClient + resource.ResourceIndex + + LastSearchRequest *resourcepb.ResourceSearchRequest + + MockResponses []*resourcepb.ResourceSearchResponse + MockCalls []*resourcepb.ResourceSearchRequest + CallCount int +} + +func (m *MockClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + m.LastSearchRequest = in + m.MockCalls = append(m.MockCalls, in) + + var response *resourcepb.ResourceSearchResponse + if m.CallCount < len(m.MockResponses) { + response = m.MockResponses[m.CallCount] + } + + m.CallCount = m.CallCount + 1 + + if response == nil { + response = &resourcepb.ResourceSearchResponse{} + } + + return response, nil +} +func (m *MockClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { + return nil, nil +} +func (m *MockClient) CountManagedObjects(ctx context.Context, in *resourcepb.CountManagedObjectsRequest, opts ...grpc.CallOption) (*resourcepb.CountManagedObjectsResponse, error) { + return nil, nil +} +func (m *MockClient) Watch(ctx context.Context, in *resourcepb.WatchRequest, opts ...grpc.CallOption) (resourcepb.ResourceStore_WatchClient, error) { + return nil, nil +} +func (m *MockClient) Delete(ctx context.Context, in *resourcepb.DeleteRequest, opts ...grpc.CallOption) (*resourcepb.DeleteResponse, error) { + return nil, nil +} +func (m *MockClient) Create(ctx context.Context, in *resourcepb.CreateRequest, opts ...grpc.CallOption) (*resourcepb.CreateResponse, error) { + return nil, nil +} +func (m *MockClient) Update(ctx context.Context, in *resourcepb.UpdateRequest, opts ...grpc.CallOption) (*resourcepb.UpdateResponse, error) { + return nil, nil +} +func (m *MockClient) Read(ctx context.Context, in *resourcepb.ReadRequest, opts ...grpc.CallOption) (*resourcepb.ReadResponse, error) { + return nil, nil +} +func (m *MockClient) GetBlob(ctx context.Context, in *resourcepb.GetBlobRequest, opts ...grpc.CallOption) (*resourcepb.GetBlobResponse, error) { + return nil, nil +} +func (m *MockClient) PutBlob(ctx context.Context, in *resourcepb.PutBlobRequest, opts ...grpc.CallOption) (*resourcepb.PutBlobResponse, error) { + return nil, nil +} +func (m *MockClient) List(ctx context.Context, in *resourcepb.ListRequest, opts ...grpc.CallOption) (*resourcepb.ListResponse, error) { + return nil, nil +} +func (m *MockClient) ListManagedObjects(ctx context.Context, in *resourcepb.ListManagedObjectsRequest, opts ...grpc.CallOption) (*resourcepb.ListManagedObjectsResponse, error) { + return nil, nil +} +func (m *MockClient) IsHealthy(ctx context.Context, in *resourcepb.HealthCheckRequest, opts ...grpc.CallOption) (*resourcepb.HealthCheckResponse, error) { + return nil, nil +} +func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) { + return nil, nil +} +func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error { + return nil +} + +func (m *MockClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, nil +} + +func TestEscapeBleveQuery(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {input: "normal", expected: "normal"}, + {input: "*", expected: "\\*"}, + {input: "?", expected: "\\?"}, + {input: "\\", expected: "\\\\"}, + {input: "\\*", expected: "\\\\\\*"}, + {input: "*\\?", expected: "\\*\\\\\\?"}, + {input: "foo*bar", expected: "foo\\*bar"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := escapeBleveQuery(tt.input) + if got != tt.expected { + t.Errorf("escapeBleveQuery(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 803b2972b85..53a146412ea 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -3,8 +3,8 @@ package user import ( "context" "fmt" - "time" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -34,22 +34,26 @@ var ( _ rest.TableConvertor = (*LegacyStore)(nil) ) -var resource = iamv0alpha1.UserResourceInfo +var userResource = iamv0alpha1.UserResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Update") + defer span.End() + if !s.enableAuthnMutation { - return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") + return nil, false, apierrors.NewMethodNotSupported(userResource.GroupResource(), "update") } ns, err := request.NamespaceInfoFrom(ctx, true) @@ -100,13 +104,16 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda // DeleteCollection implements rest.CollectionDeleter. func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { - return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "deletecollection") + return nil, apierrors.NewMethodNotSupported(userResource.GroupResource(), "deletecollection") } // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Delete") + defer span.End() + if !s.enableAuthnMutation { - return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") + return nil, false, apierrors.NewMethodNotSupported(userResource.GroupResource(), "delete") } ns, err := request.NamespaceInfoFrom(ctx, true) @@ -123,7 +130,7 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation return nil, false, err } if found == nil || len(found.Items) < 1 { - return nil, false, resource.NewNotFound(name) + return nil, false, userResource.NewNotFound(name) } userToDelete := &found.Items[0] @@ -149,7 +156,7 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation } func (s *LegacyStore) New() runtime.Object { - return resource.NewFunc() + return userResource.NewFunc() } func (s *LegacyStore) Destroy() {} @@ -159,20 +166,23 @@ func (s *LegacyStore) NamespaceScoped() bool { } func (s *LegacyStore) GetSingularName() string { - return resource.GetSingularName() + return userResource.GetSingularName() } func (s *LegacyStore) NewList() runtime.Object { - return resource.NewListFunc() + return userResource.NewListFunc() } func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return resource.TableConverter().ConvertToTable(ctx, object, tableOptions) + return userResource.TableConverter().ConvertToTable(ctx, object, tableOptions) } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.List") + defer span.End() + res, err := common.List( - ctx, resource, s.ac, common.PaginationFromListOptions(options), + ctx, userResource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.User], error) { found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ Pagination: p, @@ -206,6 +216,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -217,10 +230,10 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO Pagination: common.Pagination{Limit: 1}, }) if found == nil || err != nil { - return nil, resource.NewNotFound(name) + return nil, userResource.NewNotFound(name) } if len(found.Items) < 1 { - return nil, resource.NewNotFound(name) + return nil, userResource.NewNotFound(name) } obj := toUserItem(&found.Items[0], ns.Value) @@ -229,8 +242,11 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Create") + defer span.End() + if !s.enableAuthnMutation { - return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") + return nil, apierrors.NewMethodNotSupported(userResource.GroupResource(), "create") } ns, err := request.NamespaceInfoFrom(ctx, true) @@ -293,18 +309,12 @@ func toUserItem(u *common.UserWithRole, ns string) iamv0alpha1.User { Provisioned: u.IsProvisioned, Role: u.Role, }, + Status: iamv0alpha1.UserStatus{ + LastSeenAt: u.LastSeenAt.Unix(), + }, } obj, _ := utils.MetaAccessor(item) obj.SetUpdatedTimestamp(&u.Updated) - obj.SetAnnotation(AnnoKeyLastSeenAt, formatTime(&u.LastSeenAt)) obj.SetDeprecatedInternalID(u.ID) // nolint:staticcheck return *item } - -func formatTime(v *time.Time) string { - txt := "" - if v != nil && v.Unix() != 0 { - txt = v.UTC().Format(time.RFC3339) - } - return txt -} diff --git a/pkg/registry/apis/iam/user/validate.go b/pkg/registry/apis/iam/user/validate.go index fb4e0791689..1fa1d28a875 100644 --- a/pkg/registry/apis/iam/user/validate.go +++ b/pkg/registry/apis/iam/user/validate.go @@ -128,7 +128,7 @@ func validateEmail(ctx context.Context, searchClient resourcepb.ResourceIndexCli Operator: string(selection.Equals), Values: []string{email}, }, - }, []string{"name", "email", "login"}) + }, []string{"fields.email", "fields.login"}) resp, err := searchClient.Search(ctx, req) if err != nil { @@ -159,7 +159,7 @@ func validateLogin(ctx context.Context, searchClient resourcepb.ResourceIndexCli Operator: string(selection.Equals), Values: []string{login}, }, - }, []string{"name", "email", "login"}) + }, []string{"fields.email", "fields.login"}) resp, err := searchClient.Search(ctx, req) if err != nil { return err diff --git a/pkg/registry/apis/iam/user/validate_test.go b/pkg/registry/apis/iam/user/validate_test.go index fb558912ab0..17909f2ca7a 100644 --- a/pkg/registry/apis/iam/user/validate_test.go +++ b/pkg/registry/apis/iam/user/validate_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/authlib/types" iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -178,7 +178,7 @@ func TestValidateOnCreate(t *testing.T) { IsGrafanaAdmin: false, }, searchClient: &FakeUserLegacySearchClient{ - Users: []*user.UserSearchHitDTO{ + Users: []*org.OrgUserDTO{ {Email: "existing@example"}, }, }, @@ -202,7 +202,7 @@ func TestValidateOnCreate(t *testing.T) { IsGrafanaAdmin: false, }, searchClient: &FakeUserLegacySearchClient{ - Users: []*user.UserSearchHitDTO{ + Users: []*org.OrgUserDTO{ {Login: "existinguser"}, }, }, @@ -490,7 +490,7 @@ func TestValidateOnUpdate(t *testing.T) { IsGrafanaAdmin: true, }, searchClient: &FakeUserLegacySearchClient{ - Users: []*user.UserSearchHitDTO{ + Users: []*org.OrgUserDTO{ {Email: "two@example"}, }, }, @@ -516,7 +516,7 @@ func TestValidateOnUpdate(t *testing.T) { IsGrafanaAdmin: true, }, searchClient: &FakeUserLegacySearchClient{ - Users: []*user.UserSearchHitDTO{ + Users: []*org.OrgUserDTO{ {Name: "other", UID: "uid456", Login: "two"}, }, }, @@ -536,7 +536,7 @@ func TestValidateOnUpdate(t *testing.T) { IsGrafanaAdmin: true, }, searchClient: &FakeUserLegacySearchClient{ - Users: []*user.UserSearchHitDTO{ + Users: []*org.OrgUserDTO{ {Login: "testuser", Email: "test@example"}, }, }, diff --git a/pkg/registry/apis/preferences/register.go b/pkg/registry/apis/preferences/register.go index 8964b0e0cd7..e0e6ff947fc 100644 --- a/pkg/registry/apis/preferences/register.go +++ b/pkg/registry/apis/preferences/register.go @@ -42,12 +42,6 @@ func RegisterAPIService( users user.Service, apiregistration builder.APIRegistrar, ) *APIBuilder { - // Requires development settings and clearly experimental - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil - } - sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db)) builder := &APIBuilder{ merger: newMerger(cfg, sql), diff --git a/pkg/registry/apis/provisioning/accesscontrol.go b/pkg/registry/apis/provisioning/accesscontrol.go index e56ab7b06e2..755eabb60da 100644 --- a/pkg/registry/apis/provisioning/accesscontrol.go +++ b/pkg/registry/apis/provisioning/accesscontrol.go @@ -12,6 +12,12 @@ const ( ActionProvisioningRepositoriesRead = "provisioning.repositories:read" // GET + LIST. ActionProvisioningRepositoriesDelete = "provisioning.repositories:delete" // DELETE. + // Connections + ActionProvisioningConnectionsCreate = "provisioning.connections:create" // CREATE. + ActionProvisioningConnectionsWrite = "provisioning.connections:write" // UPDATE. + ActionProvisioningConnectionsRead = "provisioning.connections:read" // GET + LIST. + ActionProvisioningConnectionsDelete = "provisioning.connections:delete" // DELETE. + // Jobs ActionProvisioningJobsCreate = "provisioning.jobs:create" // CREATE. ActionProvisioningJobsWrite = "provisioning.jobs:write" // UPDATE. @@ -20,6 +26,12 @@ const ( // Historic Jobs ActionProvisioningHistoricJobsRead = "provisioning.historicjobs:read" // GET + LIST. + + // Settings (read-only, needed by multiple UI pages) + ActionProvisioningSettingsRead = "provisioning.settings:read" // GET + LIST. + + // Stats (read-only, admin-only) + ActionProvisioningStatsRead = "provisioning.stats:read" // GET + LIST. ) func registerAccessControlRoles(service accesscontrol.Service) error { @@ -63,6 +75,46 @@ func registerAccessControlRoles(service accesscontrol.Service) error { Grants: []string{string(org.RoleAdmin)}, } + // Connections + connectionsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.connections:reader", + DisplayName: "Connections Reader", + Description: "Read and list provisioning connections.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningConnectionsRead, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + connectionsWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.connections:writer", + DisplayName: "Connections Writer", + Description: "Create, update and delete provisioning connections.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningConnectionsCreate, + }, + { + Action: ActionProvisioningConnectionsRead, + }, + { + Action: ActionProvisioningConnectionsWrite, + }, + { + Action: ActionProvisioningConnectionsDelete, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + // Jobs jobsReader := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ @@ -119,11 +171,47 @@ func registerAccessControlRoles(service accesscontrol.Service) error { Grants: []string{string(org.RoleAdmin)}, } + // Settings - granted to Viewer (accessible by all logged-in users) + settingsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.settings:reader", + DisplayName: "Settings Reader", + Description: "Read provisioning settings.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningSettingsRead, + }, + }, + }, + Grants: []string{string(org.RoleViewer)}, + } + + // Stats - granted to Admin only + statsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.stats:reader", + DisplayName: "Stats Reader", + Description: "Read provisioning stats.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningStatsRead, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + return service.DeclareFixedRoles( repositoriesReader, repositoriesWriter, + connectionsReader, + connectionsWriter, jobsReader, jobsWriter, historicJobsReader, + settingsReader, + statsReader, ) } diff --git a/pkg/registry/apis/provisioning/controller/health.go b/pkg/registry/apis/provisioning/controller/health.go index ed5f19c6ed0..a35846ed20e 100644 --- a/pkg/registry/apis/provisioning/controller/health.go +++ b/pkg/registry/apis/provisioning/controller/health.go @@ -26,6 +26,18 @@ type StatusPatcher interface { Patch(ctx context.Context, repo *provisioning.Repository, patchOperations ...map[string]interface{}) error } +// HealthCheckerInterface defines the interface for health checking operations +// +//go:generate mockery --name=HealthCheckerInterface --structname=MockHealthChecker +type HealthCheckerInterface interface { + ShouldCheckHealth(repo *provisioning.Repository) bool + RefreshHealth(ctx context.Context, repo repository.Repository) (*provisioning.TestResults, provisioning.HealthStatus, error) + RefreshHealthWithPatchOps(ctx context.Context, repo repository.Repository) (*provisioning.TestResults, provisioning.HealthStatus, []map[string]interface{}, error) + RefreshTimestamp(ctx context.Context, repo *provisioning.Repository) error + RecordFailure(ctx context.Context, failureType provisioning.HealthFailureType, err error, repo *provisioning.Repository) error + HasRecentFailure(healthStatus provisioning.HealthStatus, failureType provisioning.HealthFailureType) bool +} + // HealthChecker provides unified health checking for repositories type HealthChecker struct { statusPatcher StatusPatcher @@ -162,6 +174,33 @@ func (hc *HealthChecker) RefreshHealth(ctx context.Context, repo repository.Repo return testResults, newHealthStatus, nil } +// RefreshHealthWithPatchOps performs a health check on an existing repository +// and returns the test results, health status, and patch operations to apply. +// This method does NOT apply the patch itself, allowing the caller to batch +// multiple status updates together to avoid race conditions. +func (hc *HealthChecker) RefreshHealthWithPatchOps(ctx context.Context, repo repository.Repository) (*provisioning.TestResults, provisioning.HealthStatus, []map[string]interface{}, error) { + cfg := repo.Config() + + // Use health checker to perform comprehensive health check with existing status + testResults, newHealthStatus, err := hc.refreshHealth(ctx, repo, cfg.Status.Health) + if err != nil { + return nil, provisioning.HealthStatus{}, nil, fmt.Errorf("health check failed: %w", err) + } + + var patchOps []map[string]interface{} + + // Only return patch operation if health status actually changed + if hc.hasHealthStatusChanged(cfg.Status.Health, newHealthStatus) { + patchOps = append(patchOps, map[string]interface{}{ + "op": "replace", + "path": "/status/health", + "value": newHealthStatus, + }) + } + + return testResults, newHealthStatus, patchOps, nil +} + // RefreshTimestamp updates the health status timestamp without changing other fields func (hc *HealthChecker) RefreshTimestamp(ctx context.Context, repo *provisioning.Repository) error { // Update the timestamp on the existing health status diff --git a/pkg/registry/apis/provisioning/controller/health_test.go b/pkg/registry/apis/provisioning/controller/health_test.go index 91a0a67a131..003f14bcee6 100644 --- a/pkg/registry/apis/provisioning/controller/health_test.go +++ b/pkg/registry/apis/provisioning/controller/health_test.go @@ -532,6 +532,136 @@ func TestRefreshHealth(t *testing.T) { } } +func TestRefreshHealthWithPatchOps(t *testing.T) { + tests := []struct { + name string + testResult *provisioning.TestResults + testError error + existingStatus provisioning.HealthStatus + expectError bool + expectedHealth bool + expectPatchOps bool + expectedPatchPath string + }{ + { + name: "successful health check with status change", + testResult: &provisioning.TestResults{ + Success: true, + Code: 200, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: false, + Error: provisioning.HealthFailureHealth, + Checked: time.Now().Add(-time.Hour).UnixMilli(), + }, + expectError: false, + expectedHealth: true, + expectPatchOps: true, + expectedPatchPath: "/status/health", + }, + { + name: "failed health check with status change", + testResult: &provisioning.TestResults{ + Success: false, + Code: 500, + Errors: []provisioning.ErrorDetails{ + {Detail: "connection failed"}, + }, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: true, + Checked: time.Now().Add(-time.Hour).UnixMilli(), + }, + expectError: false, + expectedHealth: false, + expectPatchOps: true, + expectedPatchPath: "/status/health", + }, + { + name: "no status change - no patch ops returned", + testResult: &provisioning.TestResults{ + Success: true, + Code: 200, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: true, + Checked: time.Now().Add(-15 * time.Second).UnixMilli(), + }, + expectError: false, + expectedHealth: true, + expectPatchOps: false, + }, + { + name: "test repository error", + testResult: nil, + testError: errors.New("repository test failed"), + existingStatus: provisioning.HealthStatus{ + Healthy: true, + Checked: time.Now().Add(-time.Hour).UnixMilli(), + }, + expectError: true, + expectedHealth: false, + expectPatchOps: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock repository + mockRepo := &mockRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repository", + Type: provisioning.LocalRepositoryType, + }, + Status: provisioning.RepositoryStatus{ + Health: tt.existingStatus, + }, + }, + testResult: tt.testResult, + testError: tt.testError, + } + + // Create health checker with validator and tester + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(nil, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) + + // Call RefreshHealthWithPatchOps + testResults, healthStatus, patchOps, err := hc.RefreshHealthWithPatchOps(context.Background(), mockRepo) + + // Verify error + if tt.expectError { + assert.Error(t, err) + assert.Nil(t, testResults) + return + } + assert.NoError(t, err) + + // Verify health status + assert.Equal(t, tt.expectedHealth, healthStatus.Healthy) + + // Verify patch operations + if tt.expectPatchOps { + assert.NotEmpty(t, patchOps, "expected patch operations to be returned") + assert.Len(t, patchOps, 1) + assert.Equal(t, "replace", patchOps[0]["op"]) + assert.Equal(t, tt.expectedPatchPath, patchOps[0]["path"]) + assert.Equal(t, healthStatus, patchOps[0]["value"]) + } else { + assert.Empty(t, patchOps, "expected no patch operations to be returned") + } + + // Verify test results + if tt.testResult != nil { + assert.Equal(t, tt.testResult, testResults) + } + }) + } +} + func TestHasHealthStatusChanged(t *testing.T) { tests := []struct { name string diff --git a/pkg/registry/apis/provisioning/controller/mocks/HealthCheckerInterface.go b/pkg/registry/apis/provisioning/controller/mocks/HealthCheckerInterface.go new file mode 100644 index 00000000000..e70711463bf --- /dev/null +++ b/pkg/registry/apis/provisioning/controller/mocks/HealthCheckerInterface.go @@ -0,0 +1,187 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" + + v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +// MockHealthChecker is an autogenerated mock type for the HealthCheckerInterface type +type MockHealthChecker struct { + mock.Mock +} + +// HasRecentFailure provides a mock function with given fields: healthStatus, failureType +func (_m *MockHealthChecker) HasRecentFailure(healthStatus v0alpha1.HealthStatus, failureType v0alpha1.HealthFailureType) bool { + ret := _m.Called(healthStatus, failureType) + + if len(ret) == 0 { + panic("no return value specified for HasRecentFailure") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(v0alpha1.HealthStatus, v0alpha1.HealthFailureType) bool); ok { + r0 = rf(healthStatus, failureType) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// RecordFailure provides a mock function with given fields: ctx, failureType, err, repo +func (_m *MockHealthChecker) RecordFailure(ctx context.Context, failureType v0alpha1.HealthFailureType, err error, repo *v0alpha1.Repository) error { + ret := _m.Called(ctx, failureType, err, repo) + + if len(ret) == 0 { + panic("no return value specified for RecordFailure") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.HealthFailureType, error, *v0alpha1.Repository) error); ok { + r0 = rf(ctx, failureType, err, repo) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RefreshHealth provides a mock function with given fields: ctx, repo +func (_m *MockHealthChecker) RefreshHealth(ctx context.Context, repo repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, error) { + ret := _m.Called(ctx, repo) + + if len(ret) == 0 { + panic("no return value specified for RefreshHealth") + } + + var r0 *v0alpha1.TestResults + var r1 v0alpha1.HealthStatus + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, error)); ok { + return rf(ctx, repo) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) *v0alpha1.TestResults); ok { + r0 = rf(ctx, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.TestResults) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.Repository) v0alpha1.HealthStatus); ok { + r1 = rf(ctx, repo) + } else { + r1 = ret.Get(1).(v0alpha1.HealthStatus) + } + + if rf, ok := ret.Get(2).(func(context.Context, repository.Repository) error); ok { + r2 = rf(ctx, repo) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RefreshHealthWithPatchOps provides a mock function with given fields: ctx, repo +func (_m *MockHealthChecker) RefreshHealthWithPatchOps(ctx context.Context, repo repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, []map[string]interface{}, error) { + ret := _m.Called(ctx, repo) + + if len(ret) == 0 { + panic("no return value specified for RefreshHealthWithPatchOps") + } + + var r0 *v0alpha1.TestResults + var r1 v0alpha1.HealthStatus + var r2 []map[string]interface{} + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) (*v0alpha1.TestResults, v0alpha1.HealthStatus, []map[string]interface{}, error)); ok { + return rf(ctx, repo) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository) *v0alpha1.TestResults); ok { + r0 = rf(ctx, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.TestResults) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.Repository) v0alpha1.HealthStatus); ok { + r1 = rf(ctx, repo) + } else { + r1 = ret.Get(1).(v0alpha1.HealthStatus) + } + + if rf, ok := ret.Get(2).(func(context.Context, repository.Repository) []map[string]interface{}); ok { + r2 = rf(ctx, repo) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]map[string]interface{}) + } + } + + if rf, ok := ret.Get(3).(func(context.Context, repository.Repository) error); ok { + r3 = rf(ctx, repo) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// RefreshTimestamp provides a mock function with given fields: ctx, repo +func (_m *MockHealthChecker) RefreshTimestamp(ctx context.Context, repo *v0alpha1.Repository) error { + ret := _m.Called(ctx, repo) + + if len(ret) == 0 { + panic("no return value specified for RefreshTimestamp") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository) error); ok { + r0 = rf(ctx, repo) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ShouldCheckHealth provides a mock function with given fields: repo +func (_m *MockHealthChecker) ShouldCheckHealth(repo *v0alpha1.Repository) bool { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ShouldCheckHealth") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(*v0alpha1.Repository) bool); ok { + r0 = rf(repo) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NewMockHealthChecker creates a new instance of MockHealthChecker. 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 NewMockHealthChecker(t interface { + mock.TestingT + Cleanup(func()) +}) *MockHealthChecker { + mock := &MockHealthChecker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/controller/mocks/StatusPatcher.go b/pkg/registry/apis/provisioning/controller/mocks/StatusPatcher.go index 7cfe0b737bf..9999eee9006 100644 --- a/pkg/registry/apis/provisioning/controller/mocks/StatusPatcher.go +++ b/pkg/registry/apis/provisioning/controller/mocks/StatusPatcher.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index d624879b984..b0e726135a4 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -26,7 +26,6 @@ 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/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/prometheus/client_golang/prometheus" ) @@ -53,7 +52,6 @@ type RepositoryController struct { repoLister listers.RepositoryLister repoSynced cache.InformerSynced logger logging.Logger - dualwrite dualwrite.Service jobs interface { jobs.Queue @@ -86,7 +84,6 @@ func NewRepositoryController( jobs.Queue jobs.Store }, - dualwrite dualwrite.Service, healthChecker *HealthChecker, statusPatcher StatusPatcher, registry prometheus.Registerer, @@ -114,11 +111,10 @@ func NewRepositoryController( metrics: &finalizerMetrics, maxWorkers: parallelOperations, }, - jobs: jobs, - logger: logging.DefaultLogger.With("logger", loggerName), - dualwrite: dualwrite, - registry: registry, - tracer: tracer, + jobs: jobs, + logger: logging.DefaultLogger.With("logger", loggerName), + registry: registry, + tracer: tracer, } _, err := repoInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -356,9 +352,6 @@ func (rc *RepositoryController) determineSyncStrategy(ctx context.Context, obj * case !healthStatus.Healthy: logger.Info("skip sync for unhealthy repository") return nil - case rc.dualwrite != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, rc.dualwrite): - logger.Info("skip sync as we are reading from legacy storage") - return nil case healthStatus.Healthy != obj.Status.Health.Healthy: logger.Info("repository became healthy, full resync") return &provisioning.SyncJobOptions{} @@ -561,11 +554,16 @@ func (rc *RepositoryController) process(item *queueItem) error { } // Handle health checks using the health checker - _, healthStatus, err := rc.healthChecker.RefreshHealth(ctx, repo) + _, healthStatus, healthPatchOps, err := rc.healthChecker.RefreshHealthWithPatchOps(ctx, repo) if err != nil { return fmt.Errorf("update health status: %w", err) } + // Add health patch operations first + if len(healthPatchOps) > 0 { + patchOperations = append(patchOperations, healthPatchOps...) + } + // determine the sync strategy and sync status to apply syncOptions := rc.determineSyncStrategy(ctx, obj, repo, shouldResync, healthStatus) patchOperations = append(patchOperations, rc.determineSyncStatusOps(obj, syncOptions, healthStatus)...) diff --git a/pkg/registry/apis/provisioning/controller/repository_test.go b/pkg/registry/apis/provisioning/controller/repository_test.go index 8eab43d847f..9390d98ec34 100644 --- a/pkg/registry/apis/provisioning/controller/repository_test.go +++ b/pkg/registry/apis/provisioning/controller/repository_test.go @@ -39,6 +39,10 @@ func (m mockProvisioningV0alpha1Interface) Jobs(namespace string) client.JobInte panic("not needed for testing") } +func (m mockProvisioningV0alpha1Interface) Connections(namespace string) client.ConnectionInterface { + panic("not needed for testing") +} + func (m mockProvisioningV0alpha1Interface) Repositories(namespace string) client.RepositoryInterface { if m.repositoriesFunc != nil { return m.repositoriesFunc(namespace) @@ -350,6 +354,161 @@ type mockJobsQueueStore struct { *jobs.MockStore } +func TestRepositoryController_process_UnhealthyRepositoryStatusUpdate(t *testing.T) { + testCases := []struct { + name string + repo *provisioning.Repository + healthStatus provisioning.HealthStatus + hasHealthStatusChanged bool + expectedUnhealthyMessage bool + description string + }{ + { + name: "unhealthy repository should set unhealthy message in sync status", + repo: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + Generation: 1, + }, + Spec: provisioning.RepositorySpec{ + Sync: provisioning.SyncOptions{ + Enabled: true, + IntervalSeconds: 300, + }, + }, + Status: provisioning.RepositoryStatus{ + ObservedGeneration: 1, + Health: provisioning.HealthStatus{ + Healthy: true, + Checked: time.Now().Add(-10 * time.Minute).UnixMilli(), + }, + Sync: provisioning.SyncStatus{ + State: provisioning.JobStateSuccess, + Finished: time.Now().Add(-1 * time.Minute).UnixMilli(), + Message: []string{}, + }, + }, + }, + healthStatus: provisioning.HealthStatus{ + Healthy: false, + Error: provisioning.HealthFailureHealth, + Checked: time.Now().UnixMilli(), + Message: []string{"connection failed"}, + }, + hasHealthStatusChanged: true, + expectedUnhealthyMessage: true, + description: "should set unhealthy message when repository becomes unhealthy", + }, + { + name: "unhealthy repository should not duplicate unhealthy message", + repo: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + Generation: 1, + }, + Spec: provisioning.RepositorySpec{ + Sync: provisioning.SyncOptions{ + Enabled: true, + IntervalSeconds: 300, + }, + }, + Status: provisioning.RepositoryStatus{ + ObservedGeneration: 1, + Health: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-2 * time.Minute).UnixMilli(), + }, + Sync: provisioning.SyncStatus{ + State: provisioning.JobStateError, + Finished: time.Now().Add(-1 * time.Minute).UnixMilli(), + Message: []string{"Repository is unhealthy"}, + }, + }, + }, + healthStatus: provisioning.HealthStatus{ + Healthy: false, + Error: provisioning.HealthFailureHealth, + Checked: time.Now().UnixMilli(), + Message: []string{"connection failed"}, + }, + hasHealthStatusChanged: false, + expectedUnhealthyMessage: false, + description: "should not set unhealthy message when it already exists", + }, + { + name: "healthy repository should clear unhealthy message", + repo: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + Generation: 1, + }, + Spec: provisioning.RepositorySpec{ + Sync: provisioning.SyncOptions{ + Enabled: true, + IntervalSeconds: 300, + }, + }, + Status: provisioning.RepositoryStatus{ + ObservedGeneration: 1, + Health: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-2 * time.Minute).UnixMilli(), + }, + Sync: provisioning.SyncStatus{ + State: provisioning.JobStateError, + Finished: time.Now().Add(-1 * time.Minute).UnixMilli(), + Message: []string{"Repository is unhealthy"}, + }, + }, + }, + healthStatus: provisioning.HealthStatus{ + Healthy: true, + Checked: time.Now().UnixMilli(), + Message: []string{}, + }, + hasHealthStatusChanged: true, + expectedUnhealthyMessage: false, + description: "should clear unhealthy message when repository becomes healthy", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create controller + rc := &RepositoryController{} + + // Determine sync status ops (this is a pure function, no mocks needed) + syncOps := rc.determineSyncStatusOps(tc.repo, nil, tc.healthStatus) + + // Verify expectations + hasUnhealthyOp := false + hasClearUnhealthyOp := false + for _, op := range syncOps { + if path, ok := op["path"].(string); ok { + if path == "/status/sync/message" { + if messages, ok := op["value"].([]string); ok { + if len(messages) > 0 && messages[0] == "Repository is unhealthy" { + hasUnhealthyOp = true + } else if len(messages) == 0 { + hasClearUnhealthyOp = true + } + } + } + } + } + + if tc.expectedUnhealthyMessage { + assert.True(t, hasUnhealthyOp, tc.description+": expected unhealthy message operation") + } else if len(tc.repo.Status.Sync.Message) > 0 && tc.healthStatus.Healthy { + assert.True(t, hasClearUnhealthyOp, tc.description+": expected clear unhealthy message operation") + } + }) + } +} + func TestRepositoryController_shouldResync_StaleSyncStatus(t *testing.T) { testCases := []struct { name string diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go index ad9bc4bc472..a418860caac 100644 --- a/pkg/registry/apis/provisioning/files.go +++ b/pkg/registry/apis/provisioning/files.go @@ -13,9 +13,10 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/auth" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" - "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) @@ -26,12 +27,12 @@ const ( type filesConnector struct { getter RepoGetter - access authlib.AccessChecker + access auth.AccessChecker parsers resources.ParserFactory clients resources.ClientFactory } -func NewFilesConnector(getter RepoGetter, parsers resources.ParserFactory, clients resources.ClientFactory, access authlib.AccessChecker) *filesConnector { +func NewFilesConnector(getter RepoGetter, parsers resources.ParserFactory, clients resources.ClientFactory, access auth.AccessChecker) *filesConnector { return &filesConnector{getter: getter, parsers: parsers, clients: clients, access: access} } @@ -74,179 +75,233 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. ctx = logging.Context(ctx, logger) return WithTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - repo, err := c.getRepo(ctx, r.Method, name) - if err != nil { - logger.Debug("failed to find repository", "error", err) - responder.Error(err) - return - } - - readWriter, ok := repo.(repository.ReaderWriter) - if !ok { - responder.Error(apierrors.NewBadRequest("repository does not support read-writing")) - return - } - - parser, err := c.parsers.GetParser(ctx, readWriter) - if err != nil { - responder.Error(fmt.Errorf("failed to get parser: %w", err)) - return - } - - clients, err := c.clients.Clients(ctx, repo.Config().Namespace) - if err != nil { - responder.Error(fmt.Errorf("failed to get clients: %w", err)) - return - } - - folderClient, err := clients.Folder(ctx) - if err != nil { - responder.Error(fmt.Errorf("failed to get folder client: %w", err)) - return - } - folders := resources.NewFolderManager(readWriter, folderClient, resources.NewEmptyFolderTree()) - dualReadWriter := resources.NewDualReadWriter(readWriter, parser, folders, c.access) - query := r.URL.Query() - opts := resources.DualWriteOptions{ - Ref: query.Get("ref"), - Message: query.Get("message"), - SkipDryRun: query.Get("skipDryRun") == "true", - OriginalPath: query.Get("originalPath"), - Branch: repo.Config().Branch(), - } - logger := logger.With("url", r.URL.Path, "ref", opts.Ref, "message", opts.Message) - ctx := logging.Context(r.Context(), logger) - - opts.Path, err = pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/files", name)) - if err != nil { - responder.Error(apierrors.NewBadRequest(err.Error())) - return - } - - if err := resources.IsPathSupported(opts.Path); err != nil { - responder.Error(apierrors.NewBadRequest(err.Error())) - return - } - - isDir := safepath.IsDir(opts.Path) - if r.Method == http.MethodGet && isDir { - files, err := c.listFolderFiles(ctx, opts.Path, opts.Ref, readWriter) - if err != nil { - responder.Error(err) - return - } - - responder.Object(http.StatusOK, files) - return - } - - if opts.Path == "" { - responder.Error(apierrors.NewBadRequest("missing request path")) - return - } - - var obj *provisioning.ResourceWrapper - code := http.StatusOK - switch r.Method { - case http.MethodGet: - resource, err := dualReadWriter.Read(ctx, opts.Path, opts.Ref) - if err != nil { - respondWithError(responder, err) - return - } - obj = resource.AsResourceWrapper() - case http.MethodPost: - // Check if this is a move operation first (originalPath query parameter is present) - if opts.OriginalPath != "" { - // For move operations, only read body for file moves (not directory moves) - if !isDir { - opts.Data, err = readBody(r, filesMaxBodySize) - if err != nil { - responder.Error(err) - return - } - } - - resource, err := dualReadWriter.MoveResource(ctx, opts) - if err != nil { - respondWithError(responder, err) - return - } - obj = resource.AsResourceWrapper() - } else if isDir { - obj, err = dualReadWriter.CreateFolder(ctx, opts) - } else { - opts.Data, err = readBody(r, filesMaxBodySize) - if err != nil { - responder.Error(err) - return - } - - var resource *resources.ParsedResource - resource, err = dualReadWriter.CreateResource(ctx, opts) - if err != nil { - respondWithError(responder, err) - return - } - obj = resource.AsResourceWrapper() - } - case http.MethodPut: - // TODO: document in API specification - if isDir { - err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) - } else { - opts.Data, err = readBody(r, filesMaxBodySize) - if err != nil { - responder.Error(err) - return - } - - resource, err := dualReadWriter.UpdateResource(ctx, opts) - if err != nil { - respondWithError(responder, err) - return - } - obj = resource.AsResourceWrapper() - } - case http.MethodDelete: - resource, err := dualReadWriter.Delete(ctx, opts) - if err != nil { - respondWithError(responder, err) - return - } - obj = resource.AsResourceWrapper() - default: - err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) - } - - if err != nil { - logger.Debug("got an error after processing request", "error", err) - responder.Error(err) - return - } - - if len(obj.Errors) > 0 { - code = http.StatusPartialContent - } - - logger.Debug("request resulted in valid object", "object", obj) - responder.Object(code, obj) + c.handleRequest(ctx, name, r, responder, logger) }), 30*time.Second), nil } -// listFolderFiles returns a list of files in a folder -func (c *filesConnector) listFolderFiles(ctx context.Context, filePath string, ref string, readWriter repository.ReaderWriter) (*provisioning.FileList, error) { - id, err := identity.GetRequester(ctx) +// handleRequest processes the HTTP request for files operations. +func (c *filesConnector) handleRequest(ctx context.Context, name string, r *http.Request, responder rest.Responder, logger logging.Logger) { + repo, err := c.getRepo(ctx, r.Method, name) if err != nil { - return nil, fmt.Errorf("missing auth info in context") + logger.Debug("failed to find repository", "error", err) + responder.Error(err) + return } - // TODO: replace with access check on the repo itself - if !id.GetOrgRole().Includes(identity.RoleAdmin) { - return nil, apierrors.NewForbidden(resources.DashboardResource.GroupResource(), "", - fmt.Errorf("requires admin role")) + readWriter, ok := repo.(repository.ReaderWriter) + if !ok { + responder.Error(apierrors.NewBadRequest("repository does not support read-writing")) + return } + dualReadWriter, err := c.createDualReadWriter(ctx, repo, readWriter) + if err != nil { + responder.Error(err) + return + } + + opts, err := c.parseRequestOptions(r, name, repo) + if err != nil { + responder.Error(apierrors.NewBadRequest(err.Error())) + return + } + + logger = logger.With("url", r.URL.Path, "ref", opts.Ref, "message", opts.Message) + ctx = logging.Context(r.Context(), logger) + + // Handle directory listing separately + isDir := safepath.IsDir(opts.Path) + if r.Method == http.MethodGet && isDir { + c.handleDirectoryListing(ctx, name, opts, readWriter, responder) + return + } + + if opts.Path == "" { + responder.Error(apierrors.NewBadRequest("missing request path")) + return + } + + obj, err := c.handleMethodRequest(ctx, r, opts, isDir, dualReadWriter) + if err != nil { + logger.Debug("got an error after processing request", "error", err) + respondWithError(responder, err) + return + } + + code := http.StatusOK + if len(obj.Errors) > 0 { + code = http.StatusPartialContent + } + + logger.Debug("request resulted in valid object", "object", obj) + responder.Object(code, obj) +} + +// createDualReadWriter sets up the dual read writer with all required dependencies. +func (c *filesConnector) createDualReadWriter(ctx context.Context, repo repository.Repository, readWriter repository.ReaderWriter) (*resources.DualReadWriter, error) { + parser, err := c.parsers.GetParser(ctx, readWriter) + if err != nil { + return nil, fmt.Errorf("failed to get parser: %w", err) + } + + clients, err := c.clients.Clients(ctx, repo.Config().Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get clients: %w", err) + } + + folderClient, err := clients.Folder(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get folder client: %w", err) + } + + folders := resources.NewFolderManager(readWriter, folderClient, resources.NewEmptyFolderTree()) + return resources.NewDualReadWriter(readWriter, parser, folders, c.access), nil +} + +// parseRequestOptions extracts options from the HTTP request. +func (c *filesConnector) parseRequestOptions(r *http.Request, name string, repo repository.Repository) (resources.DualWriteOptions, error) { + query := r.URL.Query() + opts := resources.DualWriteOptions{ + Ref: query.Get("ref"), + Message: query.Get("message"), + SkipDryRun: query.Get("skipDryRun") == "true", + OriginalPath: query.Get("originalPath"), + Branch: repo.Config().Branch(), + } + + path, err := pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/files", name)) + if err != nil { + return opts, err + } + opts.Path = path + + if err := resources.IsPathSupported(opts.Path); err != nil { + return opts, err + } + + return opts, nil +} + +// handleDirectoryListing handles GET requests for directory listing. +func (c *filesConnector) handleDirectoryListing(ctx context.Context, name string, opts resources.DualWriteOptions, readWriter repository.ReaderWriter, responder rest.Responder) { + if err := c.authorizeListFiles(ctx, name); err != nil { + responder.Error(err) + return + } + + files, err := c.listFolderFiles(ctx, opts.Path, opts.Ref, readWriter) + if err != nil { + responder.Error(err) + return + } + + responder.Object(http.StatusOK, files) +} + +// handleMethodRequest routes the request to the appropriate handler based on HTTP method. +func (c *filesConnector) handleMethodRequest(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + switch r.Method { + case http.MethodGet: + return c.handleGet(ctx, opts, dualReadWriter) + case http.MethodPost: + return c.handlePost(ctx, r, opts, isDir, dualReadWriter) + case http.MethodPut: + return c.handlePut(ctx, r, opts, isDir, dualReadWriter) + case http.MethodDelete: + return c.handleDelete(ctx, opts, dualReadWriter) + default: + return nil, apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) + } +} + +func (c *filesConnector) handleGet(ctx context.Context, opts resources.DualWriteOptions, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + resource, err := dualReadWriter.Read(ctx, opts.Path, opts.Ref) + if err != nil { + return nil, err + } + return resource.AsResourceWrapper(), nil +} + +func (c *filesConnector) handlePost(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + // Check if this is a move operation (originalPath query parameter is present) + if opts.OriginalPath != "" { + return c.handleMove(ctx, r, opts, isDir, dualReadWriter) + } + + if isDir { + return dualReadWriter.CreateFolder(ctx, opts) + } + + data, err := readBody(r, filesMaxBodySize) + if err != nil { + return nil, err + } + opts.Data = data + + resource, err := dualReadWriter.CreateResource(ctx, opts) + if err != nil { + return nil, err + } + return resource.AsResourceWrapper(), nil +} + +func (c *filesConnector) handleMove(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + // For move operations, only read body for file moves (not directory moves) + if !isDir { + data, err := readBody(r, filesMaxBodySize) + if err != nil { + return nil, err + } + opts.Data = data + } + + resource, err := dualReadWriter.MoveResource(ctx, opts) + if err != nil { + return nil, err + } + return resource.AsResourceWrapper(), nil +} + +func (c *filesConnector) handlePut(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + if isDir { + return nil, apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method) + } + + data, err := readBody(r, filesMaxBodySize) + if err != nil { + return nil, err + } + opts.Data = data + + resource, err := dualReadWriter.UpdateResource(ctx, opts) + if err != nil { + return nil, err + } + return resource.AsResourceWrapper(), nil +} + +func (c *filesConnector) handleDelete(ctx context.Context, opts resources.DualWriteOptions, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) { + resource, err := dualReadWriter.Delete(ctx, opts) + if err != nil { + return nil, err + } + return resource.AsResourceWrapper(), nil +} + +// authorizeListFiles checks if the user has repositories:read permission for listing files. +// The access checker handles AccessPolicy identities, namespace resolution, and role-based fallback internally. +func (c *filesConnector) authorizeListFiles(ctx context.Context, repoName string) error { + return c.access.Check(ctx, authlib.CheckRequest{ + Verb: utils.VerbGet, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GetName(), + Name: repoName, + }, "") +} + +// listFolderFiles returns a list of files in a folder. +// Authorization is checked via authorizeListFiles before calling this function. +func (c *filesConnector) listFolderFiles(ctx context.Context, filePath string, ref string, readWriter repository.ReaderWriter) (*provisioning.FileList, error) { // TODO: Implement folder navigation if len(filePath) > 0 { return nil, apierrors.NewBadRequest("folder navigation not yet supported") diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_export_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_export_fn.go index e748228da2b..74e2ca4a26e 100644 --- a/pkg/registry/apis/provisioning/jobs/export/mock_export_fn.go +++ b/pkg/registry/apis/provisioning/jobs/export/mock_export_fn.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package export diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go index 99718fd4d88..a2582001c9a 100644 --- a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go +++ b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package export diff --git a/pkg/registry/apis/provisioning/jobs/loki_history_test.go b/pkg/registry/apis/provisioning/jobs/loki_history_test.go index 26d7f01ab4f..1f41875662c 100644 --- a/pkg/registry/apis/provisioning/jobs/loki_history_test.go +++ b/pkg/registry/apis/provisioning/jobs/loki_history_test.go @@ -49,7 +49,6 @@ func TestLokiJobHistory_WriteJob(t *testing.T) { Path: "/exported", }, Migrate: &provisioning.MigrateJobOptions{ - History: true, Message: "Migration test", }, Delete: &provisioning.DeleteJobOptions{ @@ -101,7 +100,7 @@ func TestLokiJobHistory_WriteJob(t *testing.T) { } t.Run("jobToStream creates correct stream with all fields", func(t *testing.T) { - history := createTestLokiJobHistory(t) + history := createTestLokiJobHistory() // Clean job copy like WriteJob does jobCopy := job.DeepCopy() delete(jobCopy.Labels, LabelJobClaim) @@ -147,7 +146,6 @@ func TestLokiJobHistory_WriteJob(t *testing.T) { assert.Equal(t, "main", deserializedJob.Spec.Push.Branch) assert.Equal(t, "/exported", deserializedJob.Spec.Push.Path) require.NotNil(t, deserializedJob.Spec.Migrate) - assert.True(t, deserializedJob.Spec.Migrate.History) assert.Equal(t, "Migration test", deserializedJob.Spec.Migrate.Message) require.NotNil(t, deserializedJob.Spec.Delete) assert.Equal(t, "main", deserializedJob.Spec.Delete.Ref) @@ -190,7 +188,7 @@ func TestLokiJobHistory_WriteJob(t *testing.T) { }) t.Run("buildJobQuery creates correct LogQL", func(t *testing.T) { - history := createTestLokiJobHistory(t) + history := createTestLokiJobHistory() query := history.buildJobQuery("test-ns", "test-repo") @@ -199,7 +197,7 @@ func TestLokiJobHistory_WriteJob(t *testing.T) { }) t.Run("getJobTimestamp returns correct timestamp", func(t *testing.T) { - history := createTestLokiJobHistory(t) + history := createTestLokiJobHistory() // Test finished time priority jobWithFinished := &provisioning.Job{ @@ -584,7 +582,7 @@ func TestLokiJobHistory_GetJob(t *testing.T) { } // createTestLokiJobHistory creates a LokiJobHistory for testing -func createTestLokiJobHistory(t *testing.T) *LokiJobHistory { +func createTestLokiJobHistory() *LokiJobHistory { // Create test URLs readURL, _ := url.Parse("http://localhost:3100") writeURL, _ := url.Parse("http://localhost:3100") diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go deleted file mode 100644 index 5b241c84775..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go +++ /dev/null @@ -1,95 +0,0 @@ -package migrate - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/grafana/grafana-app-sdk/logging" - 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/jobs" -) - -type LegacyMigrator struct { - legacyMigrator LegacyResourcesMigrator - storageSwapper StorageSwapper - syncWorker jobs.Worker - wrapWithStageFn WrapWithStageFn -} - -func NewLegacyMigrator( - legacyMigrator LegacyResourcesMigrator, - storageSwapper StorageSwapper, - syncWorker jobs.Worker, - wrapWithStageFn WrapWithStageFn, -) *LegacyMigrator { - return &LegacyMigrator{ - legacyMigrator: legacyMigrator, - storageSwapper: storageSwapper, - syncWorker: syncWorker, - wrapWithStageFn: wrapWithStageFn, - } -} - -func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { - namespace := rw.Config().Namespace - var stageMode repository.StageMode - if options.History { - // When History is true, we want to commit and push each file (previous PushOnWrites: true) - stageMode = repository.StageModeCommitAndPushOnEach - } else { - // When History is false, we want to commit only once (previous CommitOnlyOnce: true) - stageMode = repository.StageModeCommitOnlyOnce - } - - stageOptions := repository.StageOptions{ - Mode: stageMode, - CommitOnlyOnceMessage: options.Message, - // TODO: make this configurable - Timeout: 10 * time.Minute, - } - - // Fail if migrating at least one - progress.StrictMaxErrors(1) - progress.SetMessage(ctx, "migrating legacy resources") - - if err := m.wrapWithStageFn(ctx, rw, stageOptions, func(repo repository.Repository, staged bool) error { - rw, ok := repo.(repository.ReaderWriter) - if !ok { - return errors.New("migration job submitted targeting repository that is not a ReaderWriter") - } - - return m.legacyMigrator.Migrate(ctx, rw, namespace, options, progress) - }); err != nil { - return fmt.Errorf("migrate from SQL: %w", err) - } - - progress.SetMessage(ctx, "resetting unified storage") - if err := m.storageSwapper.WipeUnifiedAndSetMigratedFlag(ctx, namespace); err != nil { - return fmt.Errorf("unable to reset unified storage %w", err) - } - - // Reset the results after the export as pull will operate on the same resources - progress.ResetResults() - - // Delegate the import to a sync (from the already checked out go-git repository!) - progress.SetMessage(ctx, "pulling resources") - if err := m.syncWorker.Process(ctx, rw, provisioning.Job{ - Spec: provisioning.JobSpec{ - Pull: &provisioning.SyncJobOptions{ - Incremental: false, - }, - }, - }, progress); err != nil { // this will have an error when too many errors exist - progress.SetMessage(ctx, "error importing resources, reverting") - if e2 := m.storageSwapper.StopReadingUnifiedStorage(ctx); e2 != nil { - logger := logging.FromContext(ctx) - logger.Warn("error trying to revert dual write settings after an error", "err", err) - } - return err - } - - return nil -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go deleted file mode 100644 index 48add96ee16..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go +++ /dev/null @@ -1,265 +0,0 @@ -package migrate - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/runtime/schema" - - 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/utils" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" - unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" - "github.com/grafana/grafana/pkg/storage/unified/parquet" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -var _ resource.BulkResourceWriter = (*legacyResourceResourceMigrator)(nil) - -//go:generate mockery --name LegacyResourcesMigrator --structname MockLegacyResourcesMigrator --inpackage --filename mock_legacy_resources_migrator.go --with-expecter -type LegacyResourcesMigrator interface { - Migrate(ctx context.Context, rw repository.ReaderWriter, namespace string, opts provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error -} - -type legacyResourcesMigrator struct { - repositoryResources resources.RepositoryResourcesFactory - parsers resources.ParserFactory - dashboardAccess legacy.MigrationDashboardAccessor - signerFactory signature.SignerFactory - clients resources.ClientFactory - exportFn export.ExportFn -} - -func NewLegacyResourcesMigrator( - repositoryResources resources.RepositoryResourcesFactory, - parsers resources.ParserFactory, - dashboardAccess legacy.MigrationDashboardAccessor, - signerFactory signature.SignerFactory, - clients resources.ClientFactory, - exportFn export.ExportFn, -) LegacyResourcesMigrator { - return &legacyResourcesMigrator{ - repositoryResources: repositoryResources, - parsers: parsers, - dashboardAccess: dashboardAccess, - signerFactory: signerFactory, - clients: clients, - exportFn: exportFn, - } -} - -func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, namespace string, opts provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { - parser, err := m.parsers.GetParser(ctx, rw) - if err != nil { - return fmt.Errorf("get parser: %w", err) - } - - repositoryResources, err := m.repositoryResources.Client(ctx, rw) - if err != nil { - return fmt.Errorf("get repository resources: %w", err) - } - - // FIXME: signature is only relevant for repositories which support signature - // Not all repositories support history - signer, err := m.signerFactory.New(ctx, signature.SignOptions{ - Namespace: namespace, - History: opts.History, - }) - if err != nil { - return fmt.Errorf("get signer: %w", err) - } - - progress.SetMessage(ctx, "migrate folders from SQL") - clients, err := m.clients.Clients(ctx, namespace) - if err != nil { - return err - } - - // nothing special for the export for now - exportOpts := provisioning.ExportJobOptions{} - if err = m.exportFn(ctx, rw.Config().Name, exportOpts, clients, repositoryResources, progress); err != nil { - return fmt.Errorf("migrate folders from SQL: %w", err) - } - - progress.SetMessage(ctx, "migrate resources from SQL") - for _, kind := range resources.SupportedProvisioningResources { - if kind == resources.FolderResource { - continue // folders have special handling - } - - reader := newLegacyResourceMigrator( - rw, - m.dashboardAccess, - parser, - repositoryResources, - progress, - opts, - namespace, - kind.GroupResource(), - signer, - ) - - if err := reader.Migrate(ctx); err != nil { - return fmt.Errorf("migrate resource %s: %w", kind, err) - } - } - - return nil -} - -type legacyResourceResourceMigrator struct { - repo repository.ReaderWriter - dashboardAccess legacy.MigrationDashboardAccessor - parser resources.Parser - progress jobs.JobProgressRecorder - namespace string - kind schema.GroupResource - options provisioning.MigrateJobOptions - resources resources.RepositoryResources - signer signature.Signer - history map[string]string // UID >> file path -} - -func newLegacyResourceMigrator( - repo repository.ReaderWriter, - dashboardAccess legacy.MigrationDashboardAccessor, - parser resources.Parser, - resources resources.RepositoryResources, - progress jobs.JobProgressRecorder, - options provisioning.MigrateJobOptions, - namespace string, - kind schema.GroupResource, - signer signature.Signer, -) *legacyResourceResourceMigrator { - var history map[string]string - if options.History { - history = make(map[string]string) - } - return &legacyResourceResourceMigrator{ - repo: repo, - dashboardAccess: dashboardAccess, - parser: parser, - progress: progress, - options: options, - namespace: namespace, - kind: kind, - resources: resources, - signer: signer, - history: history, - } -} - -// Close implements resource.BulkResourceWriter. -func (r *legacyResourceResourceMigrator) Close() error { - return nil -} - -// CloseWithResults implements resource.BulkResourceWriter. -func (r *legacyResourceResourceMigrator) CloseWithResults() (*resourcepb.BulkResponse, error) { - return &resourcepb.BulkResponse{}, nil -} - -// Write implements resource.BulkResourceWriter. -func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourcepb.ResourceKey, value []byte) error { - // Reuse the same parse+cleanup logic - parsed, err := r.parser.Parse(ctx, &repository.FileInfo{ - Path: "", // empty path to ignore file system - Data: value, - }) - if err != nil { - return fmt.Errorf("unmarshal unstructured: %w", err) - } - - // clear anything so it will get written - parsed.Meta.SetManagerProperties(utils.ManagerProperties{}) - parsed.Meta.SetSourceProperties(utils.SourceProperties{}) - - // Add author signature to the context - ctx, err = r.signer.Sign(ctx, parsed.Meta) - if err != nil { - return fmt.Errorf("add author signature: %w", err) - } - - // TODO: this seems to be same logic as the export job - // TODO: we should use a kind safe manager here - fileName, err := r.resources.WriteResourceFileFromObject(ctx, parsed.Obj, resources.WriteOptions{ - Path: "", - Ref: "", - }) - - // When replaying history, the path to the file may change over time - // This happens when the title or folder change - if r.history != nil && err == nil { - name := parsed.Meta.GetName() - previous := r.history[name] - if previous != "" && previous != fileName { - err = r.repo.Delete(ctx, previous, "", fmt.Sprintf("moved to: %s", fileName)) - } - r.history[name] = fileName - } - - result := jobs.JobResourceResult{ - Name: parsed.Meta.GetName(), - Group: r.kind.Group, - Kind: parsed.GVK.Kind, - Action: repository.FileActionCreated, - Path: fileName, - } - - if err != nil { - result.Error = fmt.Errorf("writing resource %s/%s %s to file %s: %w", r.kind.Group, r.kind.Resource, parsed.Meta.GetName(), fileName, err) - } - - r.progress.Record(ctx, result) - if err := r.progress.TooManyErrors(); err != nil { - return err - } - - return nil -} - -func (r *legacyResourceResourceMigrator) Migrate(ctx context.Context) error { - r.progress.SetMessage(ctx, fmt.Sprintf("migrate %s resource", r.kind.Resource)) - - // Create a parquet migrator with this instance as the BulkResourceWriter - parquetClient := parquet.NewBulkResourceWriterClient(r) - migrator := unifiedmigrations.ProvideUnifiedMigratorParquet( - r.dashboardAccess, - parquetClient, - ) - - opts := legacy.MigrateOptions{ - Namespace: r.namespace, - WithHistory: r.options.History, - Resources: []schema.GroupResource{r.kind}, - OnlyCount: true, // first get the count - } - stats, err := migrator.Migrate(ctx, opts) - if err != nil { - return fmt.Errorf("unable to count legacy items %w", err) - } - - // FIXME: explain why we calculate it in this way - if len(stats.Summary) > 0 { - count := stats.Summary[0].Count // - history := stats.Summary[0].History - if history > count { - count = history // the number of items we will process - } - r.progress.SetTotal(ctx, int(count)) - } - - opts.OnlyCount = false // this time actually write - _, err = migrator.Migrate(ctx, opts) - if err != nil { - return fmt.Errorf("migrate legacy %s: %w", r.kind.Resource, err) - } - - return nil -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go deleted file mode 100644 index 7f4318639c2..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go +++ /dev/null @@ -1,934 +0,0 @@ -package migrate - -import ( - "context" - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "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" - "k8s.io/apimachinery/pkg/runtime/schema" - - 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/utils" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -func TestLegacyResourcesMigrator_Migrate(t *testing.T) { - t.Run("should fail when parser factory fails", func(t *testing.T) { - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(nil, errors.New("parser factory error")) - - signerFactory := signature.NewMockSignerFactory(t) - mockClientFactory := resources.NewMockClientFactory(t) - mockExportFn := export.NewMockExportFn(t) - - migrator := NewLegacyResourcesMigrator( - nil, - mockParserFactory, - nil, - signerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - - err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t)) - require.Error(t, err) - require.EqualError(t, err, "get parser: parser factory error") - - mockParserFactory.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - }) - - t.Run("should fail when repository resources factory fails", func(t *testing.T) { - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(resources.NewMockParser(t), nil) - - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(nil, errors.New("repo resources factory error")) - signerFactory := signature.NewMockSignerFactory(t) - mockClientFactory := resources.NewMockClientFactory(t) - mockExportFn := export.NewMockExportFn(t) - - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - nil, - signerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - - err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t)) - require.Error(t, err) - require.EqualError(t, err, "get repository resources: repo resources factory error") - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - }) - - t.Run("should fail when resource migration fails", func(t *testing.T) { - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(resources.NewMockParser(t), nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(mockRepoResources, nil) - - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, errors.New("legacy migrator error")) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - - signer := signature.NewMockSigner(t) - signerFactory := signature.NewMockSignerFactory(t) - signerFactory.On("New", mock.Anything, mock.Anything). - Return(signer, nil) - - mockClients := resources.NewMockResourceClients(t) - mockClientFactory := resources.NewMockClientFactory(t) - mockClientFactory.On("Clients", mock.Anything, "test-namespace"). - Return(mockClients, nil) - mockExportFn := export.NewMockExportFn(t) - - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - mockDashboardAccess, - signerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - Name: "test-repo", - }, - }) - mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything). - Return(nil) - - err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress) - require.Error(t, err) - require.Contains(t, err.Error(), "migrate resource") - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - mockClients.AssertExpectations(t) - repo.AssertExpectations(t) - }) - t.Run("should fail when client creation fails", func(t *testing.T) { - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(resources.NewMockParser(t), nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(mockRepoResources, nil) - - mockSigner := signature.NewMockSigner(t) - mockSignerFactory := signature.NewMockSignerFactory(t) - mockSignerFactory.On("New", mock.Anything, mock.Anything). - Return(mockSigner, nil) - - mockClientFactory := resources.NewMockClientFactory(t) - mockClientFactory.On("Clients", mock.Anything, "test-namespace"). - Return(nil, errors.New("client creation error")) - - mockExportFn := export.NewMockExportFn(t) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return() - - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - nil, - mockSignerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - - repo := repository.NewMockRepository(t) - err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress) - require.Error(t, err) - require.EqualError(t, err, "client creation error") - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockSignerFactory.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - progress.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - repo.AssertExpectations(t) - }) - - t.Run("should fail when signer factory fails", func(t *testing.T) { - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(resources.NewMockParser(t), nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(mockRepoResources, nil) - - mockSignerFactory := signature.NewMockSignerFactory(t) - mockSignerFactory.On("New", mock.Anything, signature.SignOptions{ - Namespace: "test-namespace", - History: true, - }).Return(nil, fmt.Errorf("signer factory error")) - - mockClientFactory := resources.NewMockClientFactory(t) - mockExportFn := export.NewMockExportFn(t) - - progress := jobs.NewMockJobProgressRecorder(t) - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - nil, - mockSignerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - - err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{ - History: true, - }, progress) - require.Error(t, err) - require.EqualError(t, err, "get signer: signer factory error") - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockSignerFactory.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - progress.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - }) - t.Run("should fail when folder export fails", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(mockParser, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(mockRepoResources, nil) - - mockSigner := signature.NewMockSigner(t) - mockSignerFactory := signature.NewMockSignerFactory(t) - mockSignerFactory.On("New", mock.Anything, signature.SignOptions{ - Namespace: "test-namespace", - History: false, - }).Return(mockSigner, nil) - - mockClients := resources.NewMockResourceClients(t) - mockClientFactory := resources.NewMockClientFactory(t) - mockClientFactory.On("Clients", mock.Anything, "test-namespace"). - Return(mockClients, nil) - - mockExportFn := export.NewMockExportFn(t) - mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything). - Return(fmt.Errorf("export error")) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return() - - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - nil, - mockSignerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - Name: "test-repo", - }, - }) - - err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress) - require.Error(t, err) - require.Contains(t, err.Error(), "migrate folders from SQL: export error") - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockSignerFactory.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should successfully migrate all resources", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - mockParserFactory := resources.NewMockParserFactory(t) - mockParserFactory.On("GetParser", mock.Anything, mock.Anything). - Return(mockParser, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). - Return(mockRepoResources, nil) - - mockSigner := signature.NewMockSigner(t) - mockSignerFactory := signature.NewMockSignerFactory(t) - mockSignerFactory.On("New", mock.Anything, signature.SignOptions{ - Namespace: "test-namespace", - History: true, - }).Return(mockSigner, nil) - - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - // Mock CountResources for the count phase - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() - // Mock MigrateDashboards for the actual migration phase (dashboards resource) - mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return !opts.OnlyCount && opts.Namespace == "test-namespace" - }), mock.Anything).Return(&legacy.BlobStoreInfo{ - Count: 10, - Size: 5, - }, nil).Once() - - mockClients := resources.NewMockResourceClients(t) - mockClientFactory := resources.NewMockClientFactory(t) - mockClientFactory.On("Clients", mock.Anything, "test-namespace"). - Return(mockClients, nil) - mockExportFn := export.NewMockExportFn(t) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return() - progress.On("SetMessage", mock.Anything, "migrate resources from SQL").Return() - progress.On("SetMessage", mock.Anything, "migrate dashboards resource").Return() - - migrator := NewLegacyResourcesMigrator( - mockRepoResourcesFactory, - mockParserFactory, - mockDashboardAccess, - mockSignerFactory, - mockClientFactory, - mockExportFn.Execute, - ) - - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - Name: "test-repo", - }, - }) - mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything). - Return(nil) - - err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{ - History: true, - }, progress) - require.NoError(t, err) - - mockParserFactory.AssertExpectations(t) - mockRepoResourcesFactory.AssertExpectations(t) - mockDashboardAccess.AssertExpectations(t) - mockClientFactory.AssertExpectations(t) - mockExportFn.AssertExpectations(t) - progress.AssertExpectations(t) - mockClients.AssertExpectations(t) - }) -} - -func TestLegacyResourceResourceMigrator_Write(t *testing.T) { - t.Run("should fail when parser fails", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(nil, errors.New("parser error")) - - progress := jobs.NewMockJobProgressRecorder(t) - - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - err := migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.Error(t, err) - require.Contains(t, err.Error(), "unmarshal unstructured") - - mockParser.AssertExpectations(t) - }) - - t.Run("records error when create resource file fails", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything). - Return("", errors.New("create file error")) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { - return result.Action == repository.FileActionCreated && - result.Name == "test" && - result.Error != nil && - result.Error.Error() == "writing resource test.grafana.app/tests test to file : create file error" - })).Return() - progress.On("TooManyErrors").Return(nil) - - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - mockRepoResources, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.NoError(t, err) // Error is recorded but not returned - - mockParser.AssertExpectations(t) - mockRepoResources.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should fail when signer fails", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockSigner := signature.NewMockSigner(t) - mockSigner.On("Sign", mock.Anything, meta). - Return(nil, errors.New("signing error")) - - progress := jobs.NewMockJobProgressRecorder(t) - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - mockSigner, - ) - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.Error(t, err) - require.EqualError(t, err, "add author signature: signing error") - - mockParser.AssertExpectations(t) - mockSigner.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should successfully add author signature", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockSigner := signature.NewMockSigner(t) - signedCtx := repository.WithAuthorSignature(context.Background(), repository.CommitSignature{ - Name: "test-user", - Email: "test@example.com", - }) - mockSigner.On("Sign", mock.Anything, meta). - Return(signedCtx, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResources.On("WriteResourceFileFromObject", signedCtx, mock.Anything, mock.Anything). - Return("test/path", nil) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { - return result.Action == repository.FileActionCreated && - result.Name == "test" && - result.Error == nil && - result.Path == "test/path" - })).Return() - progress.On("TooManyErrors").Return(nil) - - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - mockRepoResources, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - mockSigner, - ) - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.NoError(t, err) - - mockParser.AssertExpectations(t) - mockSigner.AssertExpectations(t) - mockRepoResources.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should maintain history", func(t *testing.T) { - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("Record", mock.Anything, mock.Anything).Return() - progress.On("TooManyErrors").Return(nil) - - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, _ := utils.MetaAccessor(obj) - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockRepo := repository.NewMockRepository(t) - - mockRepoResources := resources.NewMockRepositoryResources(t) - writeResourceFileFromObject := mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything) - - migrator := newLegacyResourceMigrator( - mockRepo, - nil, - mockParser, - mockRepoResources, - progress, - provisioning.MigrateJobOptions{ - History: true, - }, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - writeResourceFileFromObject.Return("aaaa.json", nil) - err := migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("")) - require.NoError(t, err) - require.Equal(t, "aaaa.json", migrator.history["test"], "kept track of the old files") - - // Change the result file name - writeResourceFileFromObject.Return("bbbb.json", nil) - mockRepo.On("Delete", mock.Anything, "aaaa.json", "", "moved to: bbbb.json"). - Return(nil).Once() - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("")) - require.NoError(t, err) - require.Equal(t, "bbbb.json", migrator.history["test"], "kept track of the old files") - - mockParser.AssertExpectations(t) - mockRepoResources.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should successfully write resource", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - meta.SetManagerProperties(utils.ManagerProperties{ - Kind: utils.ManagerKindRepo, - Identity: "test", - AllowsEdits: true, - Suspended: false, - }) - meta.SetSourceProperties(utils.SourceProperties{ - Path: "test", - Checksum: "test", - TimestampMillis: 1234567890, - }) - - mockParser.On("Parse", mock.Anything, mock.MatchedBy(func(info *repository.FileInfo) bool { - return info != nil && info.Path == "" && string(info.Data) == "test" - })). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool { - if obj == nil { - return false - } - if obj.GetName() != "test" { - return false - } - - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - managerProps, _ := meta.GetManagerProperties() - sourceProps, _ := meta.GetSourceProperties() - - return assert.Zero(t, sourceProps) && assert.Zero(t, managerProps) - }), resources.WriteOptions{ - Path: "", - Ref: "", - }). - Return("test/path", nil) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { - return result.Action == repository.FileActionCreated && - result.Name == "test" && - result.Error == nil && - result.Kind == "" && // empty kind - result.Group == "test.grafana.app" && - result.Path == "test/path" - })).Return() - progress.On("TooManyErrors").Return(nil) - - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - mockRepoResources, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.NoError(t, err) - - mockParser.AssertExpectations(t) - mockRepoResources.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should fail when too many errors", func(t *testing.T) { - mockParser := resources.NewMockParser(t) - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "metadata": map[string]any{ - "name": "test", - }, - }, - } - meta, err := utils.MetaAccessor(obj) - require.NoError(t, err) - - mockParser.On("Parse", mock.Anything, mock.Anything). - Return(&resources.ParsedResource{ - Meta: meta, - Obj: obj, - }, nil) - - mockRepoResources := resources.NewMockRepositoryResources(t) - mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, resources.WriteOptions{}). - Return("test/path", nil) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("Record", mock.Anything, mock.Anything).Return() - progress.On("TooManyErrors").Return(errors.New("too many errors")) - - migrator := newLegacyResourceMigrator( - nil, - nil, - mockParser, - mockRepoResources, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - err = migrator.Write(context.Background(), &resourcepb.ResourceKey{}, []byte("test")) - require.EqualError(t, err, "too many errors") - - mockParser.AssertExpectations(t) - mockRepoResources.AssertExpectations(t) - progress.AssertExpectations(t) - }) -} - -func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { - t.Run("should fail when legacy migrate count fails", func(t *testing.T) { - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, errors.New("count error")) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - - migrator := newLegacyResourceMigrator( - nil, - mockDashboardAccess, - nil, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, - signature.NewGrafanaSigner(), - ) - - err := migrator.Migrate(context.Background()) - require.Error(t, err) - require.Contains(t, err.Error(), "unable to count legacy items") - - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should fail when legacy migrate write fails", func(t *testing.T) { - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - // For test-resources GroupResource, we don't know which method it will call, but since it's not dashboards/folders/librarypanels, - // the Migrate will fail trying to map the resource type. Let's make it dashboards for this test. - mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return !opts.OnlyCount && opts.Namespace == "test-namespace" - }), mock.Anything).Return(nil, errors.New("write error")).Once() // Write phase - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - - migrator := newLegacyResourceMigrator( - nil, - mockDashboardAccess, - nil, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, - signature.NewGrafanaSigner(), - ) - - err := migrator.Migrate(context.Background()) - require.Error(t, err) - require.Contains(t, err.Error(), "migrate legacy dashboards: write error") - - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - }) - - t.Run("should successfully migrate resource", func(t *testing.T) { - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return !opts.OnlyCount && opts.Namespace == "test-namespace" - }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - - migrator := newLegacyResourceMigrator( - nil, - mockDashboardAccess, - nil, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, - signature.NewGrafanaSigner(), - ) - - err := migrator.Migrate(context.Background()) - require.NoError(t, err) - - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - }) - t.Run("should set total to history if history is greater than count", func(t *testing.T) { - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{ - Summary: []*resourcepb.BulkResponse_Summary{ - { - Group: "dashboard.grafana.app", - Resource: "dashboards", - Count: 1, - History: 100, - }, - }, - }, nil).Once() // Count phase - mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return !opts.OnlyCount && opts.Namespace == "test-namespace" - }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - progress.On("SetTotal", mock.Anything, 100).Return() - - migrator := newLegacyResourceMigrator( - nil, - mockDashboardAccess, - nil, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, - signature.NewGrafanaSigner(), - ) - - err := migrator.Migrate(context.Background()) - require.NoError(t, err) - - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - }) - t.Run("should set total to count if history is less than count", func(t *testing.T) { - mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) - mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{ - Summary: []*resourcepb.BulkResponse_Summary{ - { - Group: "dashboard.grafana.app", - Resource: "dashboards", - Count: 200, - History: 1, - }, - }, - }, nil).Once() // Count phase - mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { - return !opts.OnlyCount && opts.Namespace == "test-namespace" - }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() - progress.On("SetTotal", mock.Anything, 200).Return() - signer := signature.NewMockSigner(t) - - migrator := newLegacyResourceMigrator( - nil, - mockDashboardAccess, - nil, - nil, - progress, - provisioning.MigrateJobOptions{}, - "test-namespace", - schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, - signer, - ) - - err := migrator.Migrate(context.Background()) - require.NoError(t, err) - - mockDashboardAccess.AssertExpectations(t) - progress.AssertExpectations(t) - }) -} - -func TestLegacyResourceResourceMigrator_Close(t *testing.T) { - t.Run("should return nil error", func(t *testing.T) { - migrator := &legacyResourceResourceMigrator{} - err := migrator.Close() - require.NoError(t, err) - }) -} - -func TestLegacyResourceResourceMigrator_CloseWithResults(t *testing.T) { - t.Run("should return empty bulk response and nil error", func(t *testing.T) { - migrator := &legacyResourceResourceMigrator{} - response, err := migrator.CloseWithResults() - - require.NoError(t, err) - require.NotNil(t, response) - require.IsType(t, &resourcepb.BulkResponse{}, response) - require.Empty(t, response.Summary) - }) -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go deleted file mode 100644 index 6b16fb3da7a..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package migrate - -import ( - "context" - "errors" - "testing" - "time" - - mock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - 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/jobs" -) - -func TestWrapWithStageFn(t *testing.T) { - t.Run("should return error when repository is not a ReaderWriter", func(t *testing.T) { - // Setup - ctx := context.Background() - // Create the wrapper function that matches WrapWithCloneFn signature - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - // pass a reader to function call - repo := repository.NewMockReader(t) - return fn(repo, true) - } - - legacyFoldersMigrator := NewLegacyMigrator( - NewMockLegacyResourcesMigrator(t), - NewMockStorageSwapper(t), - jobs.NewMockWorker(t), - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - err := legacyFoldersMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "migration job submitted targeting repository that is not a ReaderWriter") - }) -} -func TestWrapWithCloneFn_Error(t *testing.T) { - t.Run("should return error when wrapFn fails", func(t *testing.T) { - // Setup - ctx := context.Background() - expectedErr := errors.New("clone failed") - - // Create the wrapper function that returns an error - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return expectedErr - } - - legacyMigrator := NewLegacyMigrator( - NewMockLegacyResourcesMigrator(t), - NewMockStorageSwapper(t), - jobs.NewMockWorker(t), - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "migrate from SQL: clone failed") - }) -} - -func TestLegacyMigrator_MigrateFails(t *testing.T) { - t.Run("should return error when legacyMigrator.Migrate fails", func(t *testing.T) { - // Setup - ctx := context.Background() - expectedErr := errors.New("migration failed") - - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything). - Return(expectedErr) - - mockStorageSwapper := NewMockStorageSwapper(t) - mockWorker := jobs.NewMockWorker(t) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return fn(rw, true) - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "migrate from SQL: migration failed") - - // Storage swapper should not be called when migration fails - mockStorageSwapper.AssertNotCalled(t, "WipeUnifiedAndSetMigratedFlag") - }) -} - -func TestLegacyMigrator_ResetUnifiedStorageFails(t *testing.T) { - t.Run("should return error when storage reset fails", func(t *testing.T) { - // Setup - ctx := context.Background() - expectedErr := errors.New("reset failed") - - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything). - Return(nil) - - mockStorageSwapper := NewMockStorageSwapper(t) - mockStorageSwapper.On("WipeUnifiedAndSetMigratedFlag", mock.Anything, "test-namespace"). - Return(expectedErr) - - mockWorker := jobs.NewMockWorker(t) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return fn(rw, true) - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "unable to reset unified storage") - - // Sync worker should not be called when reset fails - mockWorker.AssertNotCalled(t, "Process") - }) -} - -func TestLegacyMigrator_SyncFails(t *testing.T) { - t.Run("should revert storage settings when sync fails", func(t *testing.T) { - // Setup - ctx := context.Background() - expectedErr := errors.New("sync failed") - - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything). - Return(nil) - - mockStorageSwapper := NewMockStorageSwapper(t) - mockStorageSwapper.On("WipeUnifiedAndSetMigratedFlag", mock.Anything, "test-namespace"). - Return(nil) - mockStorageSwapper.On("StopReadingUnifiedStorage", mock.Anything). - Return(nil) - - mockWorker := jobs.NewMockWorker(t) - mockWorker.On("Process", mock.Anything, mock.Anything, mock.MatchedBy(func(job provisioning.Job) bool { - return job.Spec.Pull != nil && !job.Spec.Pull.Incremental - }), mock.Anything).Return(expectedErr) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return fn(rw, true) - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() - progress.On("ResetResults").Return() - progress.On("SetMessage", mock.Anything, "pulling resources").Return() - progress.On("SetMessage", mock.Anything, "error importing resources, reverting").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "sync failed") - - // Verify storage settings were reverted - mockStorageSwapper.AssertCalled(t, "StopReadingUnifiedStorage", mock.Anything) - }) - - t.Run("should handle revert failure after sync failure", func(t *testing.T) { - // Setup - ctx := context.Background() - syncErr := errors.New("sync failed") - revertErr := errors.New("revert failed") - - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything). - Return(nil) - - mockStorageSwapper := NewMockStorageSwapper(t) - mockStorageSwapper.On("WipeUnifiedAndSetMigratedFlag", mock.Anything, "test-namespace"). - Return(nil) - mockStorageSwapper.On("StopReadingUnifiedStorage", mock.Anything). - Return(revertErr) - - mockWorker := jobs.NewMockWorker(t) - mockWorker.On("Process", mock.Anything, mock.Anything, mock.MatchedBy(func(job provisioning.Job) bool { - return job.Spec.Pull != nil && !job.Spec.Pull.Incremental - }), mock.Anything).Return(syncErr) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return fn(rw, true) - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() - progress.On("ResetResults").Return() - progress.On("SetMessage", mock.Anything, "pulling resources").Return() - progress.On("SetMessage", mock.Anything, "error importing resources, reverting").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.Error(t, err) - require.Contains(t, err.Error(), "sync failed") - - // Verify both errors occurred - mockStorageSwapper.AssertCalled(t, "StopReadingUnifiedStorage", mock.Anything) - }) -} - -func TestLegacyMigrator_Success(t *testing.T) { - t.Run("should complete migration successfully", func(t *testing.T) { - // Setup - ctx := context.Background() - - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything). - Return(nil) - - mockStorageSwapper := NewMockStorageSwapper(t) - mockStorageSwapper.On("WipeUnifiedAndSetMigratedFlag", mock.Anything, "test-namespace"). - Return(nil) - - mockWorker := jobs.NewMockWorker(t) - mockWorker.On("Process", mock.Anything, mock.Anything, mock.MatchedBy(func(job provisioning.Job) bool { - return job.Spec.Pull != nil && !job.Spec.Pull.Incremental - }), mock.Anything).Return(nil) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return fn(rw, true) - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() - progress.On("ResetResults").Return() - progress.On("SetMessage", mock.Anything, "pulling resources").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(ctx, repo, provisioning.MigrateJobOptions{}, progress) - - // Assert - require.NoError(t, err) - - // Verify all expected operations were called in order - mockLegacyMigrator.AssertCalled(t, "Migrate", mock.Anything, mock.Anything, "test-namespace", mock.Anything, mock.Anything) - mockStorageSwapper.AssertCalled(t, "WipeUnifiedAndSetMigratedFlag", mock.Anything, "test-namespace") - mockWorker.AssertCalled(t, "Process", mock.Anything, mock.Anything, mock.Anything, mock.Anything) - }) -} - -func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { - t.Run("should execute beforeFn functions", func(t *testing.T) { - // Setup - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockStorageSwapper := NewMockStorageSwapper(t) - mockWorker := jobs.NewMockWorker(t) - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return errors.New("abort test here") - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - // No progress messages expected in current staging implementation - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - - // Execute - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(context.Background(), repo, provisioning.MigrateJobOptions{}, progress) - require.EqualError(t, err, "migrate from SQL: abort test here") - }) -} - -func TestLegacyMigrator_ProgressScanner(t *testing.T) { - t.Run("should update progress with scanner", func(t *testing.T) { - mockLegacyMigrator := NewMockLegacyResourcesMigrator(t) - mockStorageSwapper := NewMockStorageSwapper(t) - mockWorker := jobs.NewMockWorker(t) - - // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { - return errors.New("abort test here") - } - - legacyMigrator := NewLegacyMigrator( - mockLegacyMigrator, - mockStorageSwapper, - mockWorker, - wrapFn, - ) - - progress := jobs.NewMockJobProgressRecorder(t) - // No progress messages expected in current staging implementation - progress.On("StrictMaxErrors", 1).Return() - progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() - - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-namespace", - }, - }) - - err := legacyMigrator.Migrate(context.Background(), repo, provisioning.MigrateJobOptions{}, progress) - require.EqualError(t, err, "migrate from SQL: abort test here") - - require.Eventually(t, func() bool { - // No progress message calls expected in current staging implementation - return progress.AssertExpectations(t) - }, time.Second, 10*time.Millisecond) - }) -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_process_client.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_process_client.go deleted file mode 100644 index e16d5695c0d..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_process_client.go +++ /dev/null @@ -1,430 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - metadata "google.golang.org/grpc/metadata" - - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -// BulkStore_BulkProcessClient is an autogenerated mock type for the BulkStore_BulkProcessClient type -type BulkStore_BulkProcessClient struct { - mock.Mock -} - -type BulkStore_BulkProcessClient_Expecter struct { - mock *mock.Mock -} - -func (_m *BulkStore_BulkProcessClient) EXPECT() *BulkStore_BulkProcessClient_Expecter { - return &BulkStore_BulkProcessClient_Expecter{mock: &_m.Mock} -} - -// CloseAndRecv provides a mock function with no fields -func (_m *BulkStore_BulkProcessClient) CloseAndRecv() (*resourcepb.BulkResponse, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for CloseAndRecv") - } - - var r0 *resourcepb.BulkResponse - var r1 error - if rf, ok := ret.Get(0).(func() (*resourcepb.BulkResponse, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *resourcepb.BulkResponse); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*resourcepb.BulkResponse) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BulkStore_BulkProcessClient_CloseAndRecv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseAndRecv' -type BulkStore_BulkProcessClient_CloseAndRecv_Call struct { - *mock.Call -} - -// CloseAndRecv is a helper method to define mock.On call -func (_e *BulkStore_BulkProcessClient_Expecter) CloseAndRecv() *BulkStore_BulkProcessClient_CloseAndRecv_Call { - return &BulkStore_BulkProcessClient_CloseAndRecv_Call{Call: _e.mock.On("CloseAndRecv")} -} - -func (_c *BulkStore_BulkProcessClient_CloseAndRecv_Call) Run(run func()) *BulkStore_BulkProcessClient_CloseAndRecv_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_CloseAndRecv_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *BulkStore_BulkProcessClient_CloseAndRecv_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *BulkStore_BulkProcessClient_CloseAndRecv_Call) RunAndReturn(run func() (*resourcepb.BulkResponse, error)) *BulkStore_BulkProcessClient_CloseAndRecv_Call { - _c.Call.Return(run) - return _c -} - -// CloseSend provides a mock function with no fields -func (_m *BulkStore_BulkProcessClient) CloseSend() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for CloseSend") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BulkStore_BulkProcessClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend' -type BulkStore_BulkProcessClient_CloseSend_Call struct { - *mock.Call -} - -// CloseSend is a helper method to define mock.On call -func (_e *BulkStore_BulkProcessClient_Expecter) CloseSend() *BulkStore_BulkProcessClient_CloseSend_Call { - return &BulkStore_BulkProcessClient_CloseSend_Call{Call: _e.mock.On("CloseSend")} -} - -func (_c *BulkStore_BulkProcessClient_CloseSend_Call) Run(run func()) *BulkStore_BulkProcessClient_CloseSend_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_CloseSend_Call) Return(_a0 error) *BulkStore_BulkProcessClient_CloseSend_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_CloseSend_Call) RunAndReturn(run func() error) *BulkStore_BulkProcessClient_CloseSend_Call { - _c.Call.Return(run) - return _c -} - -// Context provides a mock function with no fields -func (_m *BulkStore_BulkProcessClient) Context() context.Context { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Context") - } - - var r0 context.Context - if rf, ok := ret.Get(0).(func() context.Context); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(context.Context) - } - } - - return r0 -} - -// BulkStore_BulkProcessClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' -type BulkStore_BulkProcessClient_Context_Call struct { - *mock.Call -} - -// Context is a helper method to define mock.On call -func (_e *BulkStore_BulkProcessClient_Expecter) Context() *BulkStore_BulkProcessClient_Context_Call { - return &BulkStore_BulkProcessClient_Context_Call{Call: _e.mock.On("Context")} -} - -func (_c *BulkStore_BulkProcessClient_Context_Call) Run(run func()) *BulkStore_BulkProcessClient_Context_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Context_Call) Return(_a0 context.Context) *BulkStore_BulkProcessClient_Context_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Context_Call) RunAndReturn(run func() context.Context) *BulkStore_BulkProcessClient_Context_Call { - _c.Call.Return(run) - return _c -} - -// Header provides a mock function with no fields -func (_m *BulkStore_BulkProcessClient) Header() (metadata.MD, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Header") - } - - var r0 metadata.MD - var r1 error - if rf, ok := ret.Get(0).(func() (metadata.MD, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() metadata.MD); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(metadata.MD) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BulkStore_BulkProcessClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header' -type BulkStore_BulkProcessClient_Header_Call struct { - *mock.Call -} - -// Header is a helper method to define mock.On call -func (_e *BulkStore_BulkProcessClient_Expecter) Header() *BulkStore_BulkProcessClient_Header_Call { - return &BulkStore_BulkProcessClient_Header_Call{Call: _e.mock.On("Header")} -} - -func (_c *BulkStore_BulkProcessClient_Header_Call) Run(run func()) *BulkStore_BulkProcessClient_Header_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Header_Call) Return(_a0 metadata.MD, _a1 error) *BulkStore_BulkProcessClient_Header_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Header_Call) RunAndReturn(run func() (metadata.MD, error)) *BulkStore_BulkProcessClient_Header_Call { - _c.Call.Return(run) - return _c -} - -// RecvMsg provides a mock function with given fields: m -func (_m *BulkStore_BulkProcessClient) RecvMsg(m interface{}) error { - ret := _m.Called(m) - - if len(ret) == 0 { - panic("no return value specified for RecvMsg") - } - - var r0 error - if rf, ok := ret.Get(0).(func(interface{}) error); ok { - r0 = rf(m) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BulkStore_BulkProcessClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg' -type BulkStore_BulkProcessClient_RecvMsg_Call struct { - *mock.Call -} - -// RecvMsg is a helper method to define mock.On call -// - m interface{} -func (_e *BulkStore_BulkProcessClient_Expecter) RecvMsg(m interface{}) *BulkStore_BulkProcessClient_RecvMsg_Call { - return &BulkStore_BulkProcessClient_RecvMsg_Call{Call: _e.mock.On("RecvMsg", m)} -} - -func (_c *BulkStore_BulkProcessClient_RecvMsg_Call) Run(run func(m interface{})) *BulkStore_BulkProcessClient_RecvMsg_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_RecvMsg_Call) Return(_a0 error) *BulkStore_BulkProcessClient_RecvMsg_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_RecvMsg_Call) RunAndReturn(run func(interface{}) error) *BulkStore_BulkProcessClient_RecvMsg_Call { - _c.Call.Return(run) - return _c -} - -// Send provides a mock function with given fields: _a0 -func (_m *BulkStore_BulkProcessClient) Send(_a0 *resourcepb.BulkRequest) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for Send") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*resourcepb.BulkRequest) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BulkStore_BulkProcessClient_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' -type BulkStore_BulkProcessClient_Send_Call struct { - *mock.Call -} - -// Send is a helper method to define mock.On call -// - _a0 *resource.BulkRequest -func (_e *BulkStore_BulkProcessClient_Expecter) Send(_a0 interface{}) *BulkStore_BulkProcessClient_Send_Call { - return &BulkStore_BulkProcessClient_Send_Call{Call: _e.mock.On("Send", _a0)} -} - -func (_c *BulkStore_BulkProcessClient_Send_Call) Run(run func(_a0 *resourcepb.BulkRequest)) *BulkStore_BulkProcessClient_Send_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*resourcepb.BulkRequest)) - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Send_Call) Return(_a0 error) *BulkStore_BulkProcessClient_Send_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Send_Call) RunAndReturn(run func(*resourcepb.BulkRequest) error) *BulkStore_BulkProcessClient_Send_Call { - _c.Call.Return(run) - return _c -} - -// SendMsg provides a mock function with given fields: m -func (_m *BulkStore_BulkProcessClient) SendMsg(m interface{}) error { - ret := _m.Called(m) - - if len(ret) == 0 { - panic("no return value specified for SendMsg") - } - - var r0 error - if rf, ok := ret.Get(0).(func(interface{}) error); ok { - r0 = rf(m) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BulkStore_BulkProcessClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg' -type BulkStore_BulkProcessClient_SendMsg_Call struct { - *mock.Call -} - -// SendMsg is a helper method to define mock.On call -// - m interface{} -func (_e *BulkStore_BulkProcessClient_Expecter) SendMsg(m interface{}) *BulkStore_BulkProcessClient_SendMsg_Call { - return &BulkStore_BulkProcessClient_SendMsg_Call{Call: _e.mock.On("SendMsg", m)} -} - -func (_c *BulkStore_BulkProcessClient_SendMsg_Call) Run(run func(m interface{})) *BulkStore_BulkProcessClient_SendMsg_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(interface{})) - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_SendMsg_Call) Return(_a0 error) *BulkStore_BulkProcessClient_SendMsg_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_SendMsg_Call) RunAndReturn(run func(interface{}) error) *BulkStore_BulkProcessClient_SendMsg_Call { - _c.Call.Return(run) - return _c -} - -// Trailer provides a mock function with no fields -func (_m *BulkStore_BulkProcessClient) Trailer() metadata.MD { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Trailer") - } - - var r0 metadata.MD - if rf, ok := ret.Get(0).(func() metadata.MD); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(metadata.MD) - } - } - - return r0 -} - -// BulkStore_BulkProcessClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer' -type BulkStore_BulkProcessClient_Trailer_Call struct { - *mock.Call -} - -// Trailer is a helper method to define mock.On call -func (_e *BulkStore_BulkProcessClient_Expecter) Trailer() *BulkStore_BulkProcessClient_Trailer_Call { - return &BulkStore_BulkProcessClient_Trailer_Call{Call: _e.mock.On("Trailer")} -} - -func (_c *BulkStore_BulkProcessClient_Trailer_Call) Run(run func()) *BulkStore_BulkProcessClient_Trailer_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Trailer_Call) Return(_a0 metadata.MD) *BulkStore_BulkProcessClient_Trailer_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *BulkStore_BulkProcessClient_Trailer_Call) RunAndReturn(run func() metadata.MD) *BulkStore_BulkProcessClient_Trailer_Call { - _c.Call.Return(run) - return _c -} - -// NewBulkStore_BulkProcessClient creates a new instance of BulkStore_BulkProcessClient. 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 NewBulkStore_BulkProcessClient(t interface { - mock.TestingT - Cleanup(func()) -}) *BulkStore_BulkProcessClient { - mock := &BulkStore_BulkProcessClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go deleted file mode 100644 index 9fa21e12ab0..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go +++ /dev/null @@ -1,113 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - grpc "google.golang.org/grpc" - - mock "github.com/stretchr/testify/mock" - - resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -// MockBulkStoreClient is an autogenerated mock type for the BulkStoreClient type -type MockBulkStoreClient struct { - mock.Mock -} - -type MockBulkStoreClient_Expecter struct { - mock *mock.Mock -} - -func (_m *MockBulkStoreClient) EXPECT() *MockBulkStoreClient_Expecter { - return &MockBulkStoreClient_Expecter{mock: &_m.Mock} -} - -// BulkProcess provides a mock function with given fields: ctx, opts -func (_m *MockBulkStoreClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for BulkProcess") - } - - var r0 resourcepb.BulkStore_BulkProcessClient - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error)); ok { - return rf(ctx, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, ...grpc.CallOption) resourcepb.BulkStore_BulkProcessClient); ok { - r0 = rf(ctx, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(resourcepb.BulkStore_BulkProcessClient) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ...grpc.CallOption) error); ok { - r1 = rf(ctx, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockBulkStoreClient_BulkProcess_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BulkProcess' -type MockBulkStoreClient_BulkProcess_Call struct { - *mock.Call -} - -// BulkProcess is a helper method to define mock.On call -// - ctx context.Context -// - opts ...grpc.CallOption -func (_e *MockBulkStoreClient_Expecter) BulkProcess(ctx interface{}, opts ...interface{}) *MockBulkStoreClient_BulkProcess_Call { - return &MockBulkStoreClient_BulkProcess_Call{Call: _e.mock.On("BulkProcess", - append([]interface{}{ctx}, opts...)...)} -} - -func (_c *MockBulkStoreClient_BulkProcess_Call) Run(run func(ctx context.Context, opts ...grpc.CallOption)) *MockBulkStoreClient_BulkProcess_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]grpc.CallOption, len(args)-1) - for i, a := range args[1:] { - if a != nil { - variadicArgs[i] = a.(grpc.CallOption) - } - } - run(args[0].(context.Context), variadicArgs...) - }) - return _c -} - -func (_c *MockBulkStoreClient_BulkProcess_Call) Return(_a0 resourcepb.BulkStore_BulkProcessClient, _a1 error) *MockBulkStoreClient_BulkProcess_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockBulkStoreClient_BulkProcess_Call) RunAndReturn(run func(context.Context, ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error)) *MockBulkStoreClient_BulkProcess_Call { - _c.Call.Return(run) - return _c -} - -// NewMockBulkStoreClient creates a new instance of MockBulkStoreClient. 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 NewMockBulkStoreClient(t interface { - mock.TestingT - Cleanup(func()) -}) *MockBulkStoreClient { - mock := &MockBulkStoreClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_legacy_resources_migrator.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_legacy_resources_migrator.go deleted file mode 100644 index c55c4e22508..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_legacy_resources_migrator.go +++ /dev/null @@ -1,91 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - jobs "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" - mock "github.com/stretchr/testify/mock" - - repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" - - v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" -) - -// MockLegacyResourcesMigrator is an autogenerated mock type for the LegacyResourcesMigrator type -type MockLegacyResourcesMigrator struct { - mock.Mock -} - -type MockLegacyResourcesMigrator_Expecter struct { - mock *mock.Mock -} - -func (_m *MockLegacyResourcesMigrator) EXPECT() *MockLegacyResourcesMigrator_Expecter { - return &MockLegacyResourcesMigrator_Expecter{mock: &_m.Mock} -} - -// Migrate provides a mock function with given fields: ctx, rw, namespace, opts, progress -func (_m *MockLegacyResourcesMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, namespace string, opts v0alpha1.MigrateJobOptions, progress jobs.JobProgressRecorder) error { - ret := _m.Called(ctx, rw, namespace, opts, progress) - - if len(ret) == 0 { - panic("no return value specified for Migrate") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.ReaderWriter, string, v0alpha1.MigrateJobOptions, jobs.JobProgressRecorder) error); ok { - r0 = rf(ctx, rw, namespace, opts, progress) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockLegacyResourcesMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate' -type MockLegacyResourcesMigrator_Migrate_Call struct { - *mock.Call -} - -// Migrate is a helper method to define mock.On call -// - ctx context.Context -// - rw repository.ReaderWriter -// - namespace string -// - opts v0alpha1.MigrateJobOptions -// - progress jobs.JobProgressRecorder -func (_e *MockLegacyResourcesMigrator_Expecter) Migrate(ctx interface{}, rw interface{}, namespace interface{}, opts interface{}, progress interface{}) *MockLegacyResourcesMigrator_Migrate_Call { - return &MockLegacyResourcesMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, rw, namespace, opts, progress)} -} - -func (_c *MockLegacyResourcesMigrator_Migrate_Call) Run(run func(ctx context.Context, rw repository.ReaderWriter, namespace string, opts v0alpha1.MigrateJobOptions, progress jobs.JobProgressRecorder)) *MockLegacyResourcesMigrator_Migrate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.ReaderWriter), args[2].(string), args[3].(v0alpha1.MigrateJobOptions), args[4].(jobs.JobProgressRecorder)) - }) - return _c -} - -func (_c *MockLegacyResourcesMigrator_Migrate_Call) Return(_a0 error) *MockLegacyResourcesMigrator_Migrate_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockLegacyResourcesMigrator_Migrate_Call) RunAndReturn(run func(context.Context, repository.ReaderWriter, string, v0alpha1.MigrateJobOptions, jobs.JobProgressRecorder) error) *MockLegacyResourcesMigrator_Migrate_Call { - _c.Call.Return(run) - return _c -} - -// NewMockLegacyResourcesMigrator creates a new instance of MockLegacyResourcesMigrator. 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 NewMockLegacyResourcesMigrator(t interface { - mock.TestingT - Cleanup(func()) -}) *MockLegacyResourcesMigrator { - mock := &MockLegacyResourcesMigrator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_migrator.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_migrator.go index f7bc66ca676..53050b99de1 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_migrator.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/mock_migrator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package migrate diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_namespace_cleaner.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_namespace_cleaner.go index 2b168bfa202..1ae1afa8ccf 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_namespace_cleaner.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/mock_namespace_cleaner.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package migrate diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_storage_swapper.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_storage_swapper.go deleted file mode 100644 index d3073c2566b..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_storage_swapper.go +++ /dev/null @@ -1,129 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockStorageSwapper is an autogenerated mock type for the StorageSwapper type -type MockStorageSwapper struct { - mock.Mock -} - -type MockStorageSwapper_Expecter struct { - mock *mock.Mock -} - -func (_m *MockStorageSwapper) EXPECT() *MockStorageSwapper_Expecter { - return &MockStorageSwapper_Expecter{mock: &_m.Mock} -} - -// StopReadingUnifiedStorage provides a mock function with given fields: ctx -func (_m *MockStorageSwapper) StopReadingUnifiedStorage(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for StopReadingUnifiedStorage") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockStorageSwapper_StopReadingUnifiedStorage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StopReadingUnifiedStorage' -type MockStorageSwapper_StopReadingUnifiedStorage_Call struct { - *mock.Call -} - -// StopReadingUnifiedStorage is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockStorageSwapper_Expecter) StopReadingUnifiedStorage(ctx interface{}) *MockStorageSwapper_StopReadingUnifiedStorage_Call { - return &MockStorageSwapper_StopReadingUnifiedStorage_Call{Call: _e.mock.On("StopReadingUnifiedStorage", ctx)} -} - -func (_c *MockStorageSwapper_StopReadingUnifiedStorage_Call) Run(run func(ctx context.Context)) *MockStorageSwapper_StopReadingUnifiedStorage_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockStorageSwapper_StopReadingUnifiedStorage_Call) Return(_a0 error) *MockStorageSwapper_StopReadingUnifiedStorage_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockStorageSwapper_StopReadingUnifiedStorage_Call) RunAndReturn(run func(context.Context) error) *MockStorageSwapper_StopReadingUnifiedStorage_Call { - _c.Call.Return(run) - return _c -} - -// WipeUnifiedAndSetMigratedFlag provides a mock function with given fields: ctx, namespace -func (_m *MockStorageSwapper) WipeUnifiedAndSetMigratedFlag(ctx context.Context, namespace string) error { - ret := _m.Called(ctx, namespace) - - if len(ret) == 0 { - panic("no return value specified for WipeUnifiedAndSetMigratedFlag") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, namespace) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WipeUnifiedAndSetMigratedFlag' -type MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call struct { - *mock.Call -} - -// WipeUnifiedAndSetMigratedFlag is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -func (_e *MockStorageSwapper_Expecter) WipeUnifiedAndSetMigratedFlag(ctx interface{}, namespace interface{}) *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call { - return &MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call{Call: _e.mock.On("WipeUnifiedAndSetMigratedFlag", ctx, namespace)} -} - -func (_c *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call) Run(run func(ctx context.Context, namespace string)) *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call) Return(_a0 error) *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call) RunAndReturn(run func(context.Context, string) error) *MockStorageSwapper_WipeUnifiedAndSetMigratedFlag_Call { - _c.Call.Return(run) - return _c -} - -// NewMockStorageSwapper creates a new instance of MockStorageSwapper. 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 NewMockStorageSwapper(t interface { - mock.TestingT - Cleanup(func()) -}) *MockStorageSwapper { - mock := &MockStorageSwapper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_stage_fn.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_stage_fn.go new file mode 100644 index 00000000000..3281004d695 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_stage_fn.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package migrate + +import ( + context "context" + + repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" + mock "github.com/stretchr/testify/mock" +) + +// MockWrapWithStageFn is an autogenerated mock type for the WrapWithStageFn type +type MockWrapWithStageFn struct { + mock.Mock +} + +type MockWrapWithStageFn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWrapWithStageFn) EXPECT() *MockWrapWithStageFn_Expecter { + return &MockWrapWithStageFn_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ctx, repo, stageOptions, fn +func (_m *MockWrapWithStageFn) Execute(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + ret := _m.Called(ctx, repo, stageOptions, fn) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error); ok { + r0 = rf(ctx, repo, stageOptions, fn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWrapWithStageFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MockWrapWithStageFn_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx context.Context +// - repo repository.Repository +// - stageOptions repository.StageOptions +// - fn func(repository.Repository , bool) error +func (_e *MockWrapWithStageFn_Expecter) Execute(ctx interface{}, repo interface{}, stageOptions interface{}, fn interface{}) *MockWrapWithStageFn_Execute_Call { + return &MockWrapWithStageFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, stageOptions, fn)} +} + +func (_c *MockWrapWithStageFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error)) *MockWrapWithStageFn_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.StageOptions), args[3].(func(repository.Repository, bool) error)) + }) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) Return(_a0 error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMockWrapWithStageFn creates a new instance of MockWrapWithStageFn. 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 NewMockWrapWithStageFn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWrapWithStageFn { + mock := &MockWrapWithStageFn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/storage.go b/pkg/registry/apis/provisioning/jobs/migrate/storage.go deleted file mode 100644 index 6d04eaa8c88..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/storage.go +++ /dev/null @@ -1,102 +0,0 @@ -package migrate - -import ( - "context" - "fmt" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - - "github.com/grafana/grafana-app-sdk/logging" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -//go:generate mockery --name BulkStoreClient --structname MockBulkStoreClient --inpackage --filename mock_bulk_store_client.go --with-expecter -//go:generate mockery --name=BulkStore_BulkProcessClient --srcpkg=github.com/grafana/grafana/pkg/storage/unified/resource --output=. --outpkg=migrate --filename=mock_bulk_process_client.go --with-expecter -type BulkStoreClient interface { - BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) -} - -//go:generate mockery --name StorageSwapper --structname MockStorageSwapper --inpackage --filename mock_storage_swapper.go --with-expecter -type StorageSwapper interface { - StopReadingUnifiedStorage(ctx context.Context) error - WipeUnifiedAndSetMigratedFlag(ctx context.Context, namespace string) error -} - -type storageSwapper struct { - // Direct access to unified storage... use carefully! - bulk BulkStoreClient - dual dualwrite.Service -} - -func NewStorageSwapper(bulk BulkStoreClient, dual dualwrite.Service) StorageSwapper { - return &storageSwapper{ - bulk: bulk, - dual: dual, - } -} - -func (s *storageSwapper) StopReadingUnifiedStorage(ctx context.Context) error { - // FIXME: dual writer is not namespaced which means that we would consider all namespaces migrated - // after one migrates - for _, gr := range resources.SupportedProvisioningResources { - status, _ := s.dual.Status(ctx, gr.GroupResource()) - status.ReadUnified = false - status.Migrated = 0 - status.Migrating = 0 - _, err := s.dual.Update(ctx, status) - if err != nil { - return err - } - } - - return nil -} - -func (s *storageSwapper) WipeUnifiedAndSetMigratedFlag(ctx context.Context, namespace string) error { - for _, gr := range resources.SupportedProvisioningResources { - status, _ := s.dual.Status(ctx, gr.GroupResource()) - if status.ReadUnified { - return fmt.Errorf("unexpected state - already using unified storage for: %s", gr) - } - if status.Migrating > 0 { - if time.Since(time.UnixMilli(status.Migrating)) < time.Second*30 { - return fmt.Errorf("another migration job is running for: %s", gr) - } - } - settings := resource.BulkSettings{ - RebuildCollection: true, // wipes everything in the collection - Collection: []*resourcepb.ResourceKey{{ - Namespace: namespace, - Group: gr.Group, - Resource: gr.Resource, - }}, - } - ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) - stream, err := s.bulk.BulkProcess(ctx) - if err != nil { - return fmt.Errorf("error clearing unified %s / %w", gr, err) - } - stats, err := stream.CloseAndRecv() - if err != nil { - return fmt.Errorf("error clearing unified %s / %w", gr, err) - } - logger := logging.FromContext(ctx) - logger.Error("cleared unified storage", "stats", stats) - - status.Migrated = time.Now().UnixMilli() // but not really... since the sync is starting - status.ReadUnified = true - status.WriteLegacy = false // keep legacy "clean" - _, err = s.dual.Update(ctx, status) - if err != nil { - return err - } - } - - return nil -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/storage_test.go b/pkg/registry/apis/provisioning/jobs/migrate/storage_test.go deleted file mode 100644 index 5e1adadcc2d..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/storage_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package migrate - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - - "google.golang.org/grpc/metadata" -) - -func TestStorageSwapper_StopReadingUnifiedStorage(t *testing.T) { - tests := []struct { - name string - setupMocks func(*MockBulkStoreClient, *dualwrite.MockService) - expectedError string - }{ - { - name: "should update status for all resources", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - for _, gr := range resources.SupportedProvisioningResources { - status := dualwrite.StorageStatus{ - ReadUnified: true, - Migrated: 123, - Migrating: 456, - } - dual.On("Status", mock.Anything, gr.GroupResource()).Return(status, nil) - dual.On("Update", mock.Anything, mock.MatchedBy(func(status dualwrite.StorageStatus) bool { - return !status.ReadUnified && status.Migrated == 0 && status.Migrating == 0 - })).Return(dualwrite.StorageStatus{}, nil) - } - }, - }, - { - name: "should fail if status update fails", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - dual.On("Status", mock.Anything, gr.GroupResource()).Return(dualwrite.StorageStatus{}, nil) - dual.On("Update", mock.Anything, mock.Anything).Return(dualwrite.StorageStatus{}, errors.New("update failed")) - }, - expectedError: "update failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bulk := NewMockBulkStoreClient(t) - dual := dualwrite.NewMockService(t) - - if tt.setupMocks != nil { - tt.setupMocks(bulk, dual) - } - - swapper := NewStorageSwapper(bulk, dual) - err := swapper.StopReadingUnifiedStorage(context.Background()) - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestStorageSwapper_WipeUnifiedAndSetMigratedFlag(t *testing.T) { - tests := []struct { - name string - setupMocks func(*MockBulkStoreClient, *dualwrite.MockService) - expectedError string - }{ - { - name: "should fail if already using unified storage", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - status := dualwrite.StorageStatus{ - ReadUnified: true, - } - dual.On("Status", mock.Anything, gr.GroupResource()).Return(status, nil) - }, - expectedError: "unexpected state - already using unified storage", - }, - { - name: "should fail if migration is in progress", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - status := dualwrite.StorageStatus{ - ReadUnified: false, - Migrating: time.Now().UnixMilli(), - } - dual.On("Status", mock.Anything, gr.GroupResource()).Return(status, nil) - }, - expectedError: "another migration job is running", - }, - { - name: "should fail if bulk process fails", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - dual.On("Status", mock.Anything, gr.GroupResource()).Return(dualwrite.StorageStatus{}, nil) - bulk.On("BulkProcess", mock.Anything, mock.Anything).Return(nil, errors.New("bulk process failed")) - }, - expectedError: "error clearing unified", - }, - { - name: "should fail if status update fails after bulk process", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - dual.On("Status", mock.Anything, gr.GroupResource()).Return(dualwrite.StorageStatus{}, nil) - - mockStream := NewBulkStore_BulkProcessClient(t) - mockStream.On("CloseAndRecv").Return(&resourcepb.BulkResponse{}, nil) - bulk.On("BulkProcess", mock.Anything, mock.Anything).Return(mockStream, nil) - - dual.On("Update", mock.Anything, mock.MatchedBy(func(status dualwrite.StorageStatus) bool { - return status.ReadUnified && !status.WriteLegacy && status.Migrated > 0 - })).Return(dualwrite.StorageStatus{}, errors.New("update failed")) - }, - expectedError: "update failed", - }, - { - name: "should fail if bulk process stream close fails", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - gr := resources.SupportedProvisioningResources[0] - dual.On("Status", mock.Anything, gr.GroupResource()).Return(dualwrite.StorageStatus{}, nil) - - mockStream := NewBulkStore_BulkProcessClient(t) - mockStream.On("CloseAndRecv").Return(nil, errors.New("stream close failed")) - bulk.On("BulkProcess", mock.Anything, mock.Anything).Return(mockStream, nil) - }, - expectedError: "error clearing unified", - }, - { - name: "should succeed with complete workflow", - setupMocks: func(bulk *MockBulkStoreClient, dual *dualwrite.MockService) { - for _, gr := range resources.SupportedProvisioningResources { - dual.On("Status", mock.Anything, gr.GroupResource()).Return(dualwrite.StorageStatus{}, nil) - - mockStream := NewBulkStore_BulkProcessClient(t) - mockStream.On("CloseAndRecv").Return(&resourcepb.BulkResponse{}, nil) - bulk.On("BulkProcess", mock.MatchedBy(func(ctx context.Context) bool { - md, ok := metadata.FromOutgoingContext(ctx) - if !ok { - return false - } - - //nolint:errcheck // hits the err != nil gotcha - settings, _ := resource.NewBulkSettings(md) - if !settings.RebuildCollection { - return false - } - if len(settings.Collection) != 1 { - return false - } - if settings.Collection[0].Namespace != "test-namespace" { - return false - } - if settings.Collection[0].Group != gr.Group { - return false - } - if settings.Collection[0].Resource != gr.Resource { - return false - } - - return true - }), mock.Anything).Return(mockStream, nil) - - dual.On("Update", mock.Anything, mock.MatchedBy(func(status dualwrite.StorageStatus) bool { - return status.ReadUnified && !status.WriteLegacy && status.Migrated > 0 - })).Return(dualwrite.StorageStatus{}, nil) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bulk := NewMockBulkStoreClient(t) - dual := dualwrite.NewMockService(t) - - if tt.setupMocks != nil { - tt.setupMocks(bulk, dual) - } - - swapper := NewStorageSwapper(bulk, dual) - err := swapper.WipeUnifiedAndSetMigratedFlag(context.Background(), "test-namespace") - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go index edd3a72f1bd..35c4de298f2 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go @@ -33,23 +33,7 @@ func NewUnifiedStorageMigrator( func (m *UnifiedStorageMigrator) Migrate(ctx context.Context, repo repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { namespace := repo.Config().GetNamespace() - // For folder-type repositories, only run sync (skip export and cleaner) - if repo.Config().Spec.Sync.Target == provisioning.SyncTargetTypeFolder { - progress.SetMessage(ctx, "pull resources") - syncJob := provisioning.Job{ - Spec: provisioning.JobSpec{ - Pull: &provisioning.SyncJobOptions{ - Incremental: false, - }, - }, - } - if err := m.syncWorker.Process(ctx, repo, syncJob, progress); err != nil { - return fmt.Errorf("pull resources: %w", err) - } - return nil - } - - // For instance-type repositories, run the full workflow: export -> sync -> clean + // Export resources first (for both folder and instance sync) progress.SetMessage(ctx, "export resources") progress.StrictMaxErrors(1) // strict as we want the entire instance to be managed @@ -67,6 +51,7 @@ func (m *UnifiedStorageMigrator) Migrate(ctx context.Context, repo repository.Re // Reset the results after the export as pull will operate on the same resources progress.ResetResults() + // Pull resources from the repository progress.SetMessage(ctx, "pull resources") syncJob := provisioning.Job{ Spec: provisioning.JobSpec{ @@ -79,9 +64,12 @@ func (m *UnifiedStorageMigrator) Migrate(ctx context.Context, repo repository.Re return fmt.Errorf("pull resources: %w", err) } - progress.SetMessage(ctx, "clean namespace") - if err := m.namespaceCleaner.Clean(ctx, namespace, progress); err != nil { - return fmt.Errorf("clean namespace: %w", err) + // For instance-type repositories, also clean the namespace + if repo.Config().Spec.Sync.Target != provisioning.SyncTargetTypeFolder { + progress.SetMessage(ctx, "clean namespace") + if err := m.namespaceCleaner.Clean(ctx, namespace, progress); err != nil { + return fmt.Errorf("clean namespace: %w", err) + } } return nil diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go index d509c1c2f81..d84e95fcebd 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go @@ -134,7 +134,7 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { expectedError: "", }, { - name: "should only run sync for folder-type repositories", + name: "should run export and sync for folder-type repositories", setupMocks: func(nc *MockNamespaceCleaner, ew *jobs.MockWorker, sw *jobs.MockWorker, pr *jobs.MockJobProgressRecorder, rw *repository.MockRepository) { rw.On("Config").Return(&provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ @@ -147,9 +147,15 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }, }) - // Export should be skipped - no export-related mocks - // Cleaner should also be skipped - no cleaner-related mocks - // Only sync job should run + // Export should run for folder-type repositories + pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() + ew.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { + return job.Spec.Push != nil + }), pr).Return(nil) + pr.On("ResetResults").Return() + // Cleaner should be skipped - no cleaner-related mocks + // Sync job should run pr.On("SetMessage", mock.Anything, "pull resources").Return() sw.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { return job.Spec.Pull != nil && !job.Spec.Pull.Incremental @@ -171,7 +177,14 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }, }) - // Only sync job should run and fail + // Export should run first + pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() + ew.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { + return job.Spec.Push != nil + }), pr).Return(nil) + pr.On("ResetResults").Return() + // Sync job should run and fail pr.On("SetMessage", mock.Anything, "pull resources").Return() sw.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { return job.Spec.Pull != nil && !job.Spec.Pull.Incremental diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker.go b/pkg/registry/apis/provisioning/jobs/migrate/worker.go index 9de5d4015e8..e1b4e916d10 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker.go @@ -7,7 +7,6 @@ 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/registry/apis/provisioning/jobs" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) //go:generate mockery --name Migrator --structname MockMigrator --inpackage --filename mock_migrator.go --with-expecter @@ -16,8 +15,6 @@ type Migrator interface { } type MigrationWorker struct { - storageStatus dualwrite.Service - legacyMigrator Migrator unifiedMigrator Migrator } @@ -27,16 +24,9 @@ func NewMigrationWorkerFromUnified(unifiedMigrator Migrator) *MigrationWorker { } } -// HACK: we should decouple the implementation of these two -func NewMigrationWorker( - legacyMigrator Migrator, - unifiedMigrator Migrator, - storageStatus dualwrite.Service, -) *MigrationWorker { +func NewMigrationWorker(unifiedMigrator Migrator) *MigrationWorker { return &MigrationWorker{ unifiedMigrator: unifiedMigrator, - legacyMigrator: legacyMigrator, - storageStatus: storageStatus, } } @@ -56,28 +46,5 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor return errors.New("migration job submitted targeting repository that is not a ReaderWriter") } - if options.History { - if repo.Config().Spec.Type != provisioning.GitHubRepositoryType { - return errors.New("history is only supported for github repositories") - } - } - - // Block migrate for legacy resources if repository type is folder - if repo.Config().Spec.Sync.Target == provisioning.SyncTargetTypeFolder { - // HACK: we should not have to check for storage existence here - if w.storageStatus != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, w.storageStatus) { - return errors.New("migration of legacy resources is not supported for folder-type repositories") - } - } - - // HACK: we should not have to check for storage existence here - if w.storageStatus != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, w.storageStatus) { - return w.legacyMigrator.Migrate(ctx, rw, *options, progress) - } - - if options.History { - return errors.New("history is not yet supported in unified storage") - } - return w.unifiedMigrator.Migrate(ctx, rw, *options, progress) } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go index 962a69a8f54..b8171a2df9d 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go @@ -2,7 +2,6 @@ package migrate import ( "context" - "errors" "testing" "github.com/stretchr/testify/assert" @@ -11,9 +10,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/apps/provisioning/pkg/repository/local" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) func TestMigrationWorker_IsSupported(t *testing.T) { @@ -42,7 +39,7 @@ func TestMigrationWorker_IsSupported(t *testing.T) { }, } - worker := NewMigrationWorker(nil, nil, nil) + worker := NewMigrationWorker(nil) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -53,7 +50,7 @@ func TestMigrationWorker_IsSupported(t *testing.T) { } func TestMigrationWorker_ProcessNotReaderWriter(t *testing.T) { - worker := NewMigrationWorker(nil, nil, nil) + worker := NewMigrationWorker(NewMockMigrator(t)) job := provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionMigrate, @@ -68,56 +65,13 @@ func TestMigrationWorker_ProcessNotReaderWriter(t *testing.T) { require.EqualError(t, err, "migration job submitted targeting repository that is not a ReaderWriter") } -func TestMigrationWorker_WithHistory(t *testing.T) { - fakeDualwrite := dualwrite.NewMockService(t) - fakeDualwrite.On("ReadFromUnified", mock.Anything, mock.Anything). - Maybe().Return(true, nil) // using unified storage - - worker := NewMigrationWorker(nil, nil, fakeDualwrite) - job := provisioning.Job{ - Spec: provisioning.JobSpec{ - Action: provisioning.JobActionMigrate, - Migrate: &provisioning.MigrateJobOptions{ - History: true, - }, - }, - } - - t.Run("fail local", func(t *testing.T) { - progressRecorder := jobs.NewMockJobProgressRecorder(t) - progressRecorder.On("SetTotal", mock.Anything, 10).Return() - - repo := local.NewRepository(&provisioning.Repository{}, nil) - err := worker.Process(context.Background(), repo, job, progressRecorder) - require.EqualError(t, err, "history is only supported for github repositories") - }) - - t.Run("fail unified", func(t *testing.T) { - progressRecorder := jobs.NewMockJobProgressRecorder(t) - progressRecorder.On("SetTotal", mock.Anything, 10).Return() - - repo := repository.NewMockRepository(t) - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "empty", // not valid - }, - }, - }) - err := worker.Process(context.Background(), repo, job, progressRecorder) - require.EqualError(t, err, "history is not yet supported in unified storage") - }) -} - func TestMigrationWorker_Process(t *testing.T) { tests := []struct { - name string - setupMocks func(*MockMigrator, *MockMigrator, *dualwrite.MockService, *jobs.MockJobProgressRecorder) - setupRepo func(*repository.MockRepository) - job provisioning.Job - expectedError string - isLegacyActive bool + name string + setupMocks func(*MockMigrator, *jobs.MockJobProgressRecorder) + setupRepo func(*repository.MockRepository) + job provisioning.Job + expectedError string }{ { name: "should fail when migrate settings are missing", @@ -127,7 +81,7 @@ func TestMigrationWorker_Process(t *testing.T) { Migrate: nil, }, }, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(um *MockMigrator, pr *jobs.MockJobProgressRecorder) { }, setupRepo: func(repo *repository.MockRepository) { // No Config() call expected since we fail before that @@ -135,150 +89,35 @@ func TestMigrationWorker_Process(t *testing.T) { expectedError: "missing migrate settings", }, { - name: "should use legacy migrator when legacy storage is active", + name: "should use unified storage migrator for instance-type repositories", job: provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionMigrate, Migrate: &provisioning.MigrateJobOptions{}, }, }, - isLegacyActive: true, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(um *MockMigrator, pr *jobs.MockJobProgressRecorder) { pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) - lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - }, - setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeInstance, - }, - }, - }) - }, - }, - { - name: "should use unified storage migrator when legacy storage is not active", - job: provisioning.Job{ - Spec: provisioning.JobSpec{ - Action: provisioning.JobActionMigrate, - Migrate: &provisioning.MigrateJobOptions{}, - }, - }, - isLegacyActive: false, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { - pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil) um.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeInstance, - }, - }, - }) + // No Config() call needed anymore }, }, { - name: "should propagate migrator errors", + name: "should allow migration for folder-type repositories", job: provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionMigrate, Migrate: &provisioning.MigrateJobOptions{}, }, }, - isLegacyActive: true, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(um *MockMigrator, pr *jobs.MockJobProgressRecorder) { pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) - lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("migration failed")) - }, - setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeInstance, - }, - }, - }) - }, - expectedError: "migration failed", - }, - { - name: "should block migration of legacy resources for folder-type repositories", - job: provisioning.Job{ - Spec: provisioning.JobSpec{ - Action: provisioning.JobActionMigrate, - Migrate: &provisioning.MigrateJobOptions{}, - }, - }, - isLegacyActive: true, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { - pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) - // legacyMigrator should not be called as we block before reaching it - }, - setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeFolder, - }, - }, - }) - }, - expectedError: "migration of legacy resources is not supported for folder-type repositories", - }, - { - name: "should allow migration of legacy resources for instance-type repositories", - job: provisioning.Job{ - Spec: provisioning.JobSpec{ - Action: provisioning.JobActionMigrate, - Migrate: &provisioning.MigrateJobOptions{}, - }, - }, - isLegacyActive: true, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { - pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) - lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - }, - setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeInstance, - }, - }, - }) - }, - expectedError: "", - }, - { - name: "should allow migration for folder-type repositories when legacy storage is not active", - job: provisioning.Job{ - Spec: provisioning.JobSpec{ - Action: provisioning.JobActionMigrate, - Migrate: &provisioning.MigrateJobOptions{}, - }, - }, - isLegacyActive: false, - setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { - pr.On("SetTotal", mock.Anything, 10).Return() - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil) um.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, setupRepo: func(repo *repository.MockRepository) { - repo.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Sync: provisioning.SyncOptions{ - Target: provisioning.SyncTargetTypeFolder, - }, - }, - }) + // No Config() call needed anymore }, expectedError: "", }, @@ -286,15 +125,13 @@ func TestMigrationWorker_Process(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - legacyMigrator := NewMockMigrator(t) unifiedMigrator := NewMockMigrator(t) - dualWriteService := dualwrite.NewMockService(t) progressRecorder := jobs.NewMockJobProgressRecorder(t) - worker := NewMigrationWorker(legacyMigrator, unifiedMigrator, dualWriteService) + worker := NewMigrationWorker(unifiedMigrator) if tt.setupMocks != nil { - tt.setupMocks(legacyMigrator, unifiedMigrator, dualWriteService, progressRecorder) + tt.setupMocks(unifiedMigrator, progressRecorder) } rw := repository.NewMockRepository(t) @@ -310,7 +147,7 @@ func TestMigrationWorker_Process(t *testing.T) { require.NoError(t, err) } - mock.AssertExpectationsForObjects(t, legacyMigrator, unifiedMigrator, dualWriteService, progressRecorder, rw) + mock.AssertExpectationsForObjects(t, unifiedMigrator, progressRecorder, rw) }) } } diff --git a/pkg/registry/apis/provisioning/jobs/progress.go b/pkg/registry/apis/provisioning/jobs/progress.go index 97a293a5d8c..2cb9dc9ddcf 100644 --- a/pkg/registry/apis/provisioning/jobs/progress.go +++ b/pkg/registry/apis/provisioning/jobs/progress.go @@ -35,12 +35,13 @@ func maybeNotifyProgress(threshold time.Duration, fn ProgressFn) ProgressFn { // FIXME: ProgressRecorder should be initialized in the queue type JobResourceResult struct { - Name string - Group string - Kind string - Path string - Action repository.FileAction - Error error + Name string + Group string + Kind string + Path string + Action repository.FileAction + Error error + Warning error } type jobProgressRecorder struct { @@ -193,6 +194,10 @@ func (r *jobProgressRecorder) updateSummary(result JobResourceResult) { errorMsg := fmt.Sprintf("%s (file: %s, name: %s, action: %s)", result.Error.Error(), result.Path, result.Name, result.Action) summary.Errors = append(summary.Errors, errorMsg) summary.Error++ + } else if result.Warning != nil { + warningMsg := fmt.Sprintf("%s (file: %s, name: %s, action: %s)", result.Warning.Error(), result.Path, result.Name, result.Action) + summary.Warnings = append(summary.Warnings, warningMsg) + summary.Warning++ } else { switch result.Action { case repository.FileActionDeleted: @@ -266,8 +271,17 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision jobStatus.Message = err.Error() } - jobStatus.Summary = r.summary() + summaries := r.summary() + jobStatus.Summary = summaries jobStatus.Errors = r.errors + + // Extract warnings from summaries + warnings := make([]string, 0) + for _, summary := range summaries { + warnings = append(warnings, summary.Warnings...) + } + jobStatus.Warnings = warnings + jobStatus.URLs = r.refURLs tooManyErrors := r.maxErrors > 0 && r.errorCount >= r.maxErrors @@ -283,6 +297,9 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision jobStatus.Message = "completed with errors" jobStatus.State = provisioning.JobStateWarning } + } else if len(jobStatus.Warnings) > 0 { + jobStatus.State = provisioning.JobStateWarning + jobStatus.Message = "completed with warnings" } // Override message if progress have a more explicit message diff --git a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go index 8c68bec33f7..9fde7042601 100644 --- a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/progress_test.go b/pkg/registry/apis/provisioning/jobs/progress_test.go index caf44c767ff..7e849491bbe 100644 --- a/pkg/registry/apis/provisioning/jobs/progress_test.go +++ b/pkg/registry/apis/provisioning/jobs/progress_test.go @@ -2,9 +2,11 @@ package jobs import ( "context" + "errors" "testing" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -83,3 +85,170 @@ func TestJobProgressRecorderCompleteIncludesRefURLs(t *testing.T) { assert.Equal(t, provisioning.JobStateSuccess, finalStatus.State) assert.Equal(t, "completed successfully", finalStatus.Message) } + +func TestJobProgressRecorderWarningStatus(t *testing.T) { + ctx := context.Background() + + // Create a progress recorder + mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error { + return nil + } + recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder) + + // Record a result with a warning + warningErr := errors.New("deprecated API used") + result := JobResourceResult{ + Name: "test-resource", + Group: "test.grafana.app", + Kind: "Dashboard", + Path: "dashboards/test.json", + Action: repository.FileActionUpdated, + Warning: warningErr, + } + recorder.Record(ctx, result) + + // Record another result with a different warning + warningErr2 := errors.New("missing optional field") + result2 := JobResourceResult{ + Name: "test-resource-2", + Group: "test.grafana.app", + Kind: "Dashboard", + Path: "dashboards/test2.json", + Action: repository.FileActionCreated, + Warning: warningErr2, + } + recorder.Record(ctx, result2) + + // Record a result with a warning from a different resource type + warningErr3 := errors.New("validation warning") + result3 := JobResourceResult{ + Name: "test-resource-3", + Group: "test.grafana.app", + Kind: "DataSource", + Path: "datasources/test.yaml", + Action: repository.FileActionCreated, + Warning: warningErr3, + } + recorder.Record(ctx, result3) + + // Verify warnings are stored in summaries + recorder.mu.RLock() + require.Len(t, recorder.summaries, 2) // Dashboard and DataSource + dashboardSummary := recorder.summaries["test.grafana.app:Dashboard"] + require.NotNil(t, dashboardSummary) + assert.Equal(t, int64(2), dashboardSummary.Warning) + assert.Len(t, dashboardSummary.Warnings, 2) + assert.Contains(t, dashboardSummary.Warnings[0], "deprecated API used") + assert.Contains(t, dashboardSummary.Warnings[1], "missing optional field") + + datasourceSummary := recorder.summaries["test.grafana.app:DataSource"] + require.NotNil(t, datasourceSummary) + assert.Equal(t, int64(1), datasourceSummary.Warning) + assert.Len(t, datasourceSummary.Warnings, 1) + assert.Contains(t, datasourceSummary.Warnings[0], "validation warning") + recorder.mu.RUnlock() + + // Complete the job + finalStatus := recorder.Complete(ctx, nil) + + // Verify the final status includes warnings + require.NotNil(t, finalStatus.Warnings) + assert.Len(t, finalStatus.Warnings, 3) + expectedWarnings := []string{ + "deprecated API used (file: dashboards/test.json, name: test-resource, action: updated)", + "missing optional field (file: dashboards/test2.json, name: test-resource-2, action: created)", + "validation warning (file: datasources/test.yaml, name: test-resource-3, action: created)", + } + assert.ElementsMatch(t, finalStatus.Warnings, expectedWarnings) + + // Verify the state is set to Warning + assert.Equal(t, provisioning.JobStateWarning, finalStatus.State) + assert.Equal(t, "completed with warnings", finalStatus.Message) + + // Verify summaries are included + require.Len(t, finalStatus.Summary, 2) + + // Verify no errors were recorded + assert.Empty(t, finalStatus.Errors) +} + +func TestJobProgressRecorderWarningWithErrors(t *testing.T) { + ctx := context.Background() + + // Create a progress recorder + mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error { + return nil + } + recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder) + + // Record a result with an error (errors take precedence) + errorErr := errors.New("failed to process") + result := JobResourceResult{ + Name: "test-resource", + Group: "test.grafana.app", + Kind: "Dashboard", + Path: "dashboards/test.json", + Action: repository.FileActionUpdated, + Error: errorErr, + } + recorder.Record(ctx, result) + + // Record a result with only a warning + warningErr := errors.New("deprecated API used") + result2 := JobResourceResult{ + Name: "test-resource-2", + Group: "test.grafana.app", + Kind: "Dashboard", + Path: "dashboards/test2.json", + Action: repository.FileActionCreated, + Warning: warningErr, + } + recorder.Record(ctx, result2) + + // Complete the job + finalStatus := recorder.Complete(ctx, nil) + + // When there are errors, the state should be Warning (not Error unless too many) + // and warnings should still be included + assert.Equal(t, provisioning.JobStateWarning, finalStatus.State) + assert.Equal(t, "completed with errors", finalStatus.Message) + assert.Len(t, finalStatus.Errors, 1) + assert.Contains(t, finalStatus.Errors[0], "failed to process") + + // Warnings should still be extracted from summaries + require.NotNil(t, finalStatus.Warnings) + assert.Len(t, finalStatus.Warnings, 1) + assert.Contains(t, finalStatus.Warnings[0], "deprecated API used") +} + +func TestJobProgressRecorderWarningOnlyNoErrors(t *testing.T) { + ctx := context.Background() + + // Create a progress recorder + mockProgressFn := func(ctx context.Context, status provisioning.JobStatus) error { + return nil + } + recorder := newJobProgressRecorder(mockProgressFn).(*jobProgressRecorder) + + // Record only warnings, no errors + warningErr := errors.New("deprecated API used") + result := JobResourceResult{ + Name: "test-resource", + Group: "test.grafana.app", + Kind: "Dashboard", + Path: "dashboards/test.json", + Action: repository.FileActionUpdated, + Warning: warningErr, + } + recorder.Record(ctx, result) + + // Complete the job + finalStatus := recorder.Complete(ctx, nil) + + // Verify the state is Warning (not Error) when only warnings exist + assert.Equal(t, provisioning.JobStateWarning, finalStatus.State) + assert.Equal(t, "completed with warnings", finalStatus.Message) + assert.Empty(t, finalStatus.Errors) + require.NotNil(t, finalStatus.Warnings) + assert.Len(t, finalStatus.Warnings, 1) +} diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync.go b/pkg/registry/apis/provisioning/jobs/sync/sync.go index 7f677a8ce3b..6fec2783a6a 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync.go @@ -14,7 +14,7 @@ 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, metrics jobs.JobMetrics) error -//go:generate mockery --name CompareFn --structname MockCompareFn --inpackage --filename compare_fn_mock.go --with-expecter +//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 diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 05e6340b193..367ed672bd2 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -29,9 +28,6 @@ type SyncWorker struct { // ResourceClients for the repository repositoryResources resources.RepositoryResourcesFactory - // Check if the system is using unified storage - storageStatus dualwrite.Service - // Patch status for the repository patchStatus RepositoryPatchFn @@ -48,7 +44,6 @@ type SyncWorker struct { func NewSyncWorker( clients resources.ClientFactory, repositoryResources resources.RepositoryResourcesFactory, - storageStatus dualwrite.Service, patchStatus RepositoryPatchFn, syncer Syncer, metrics jobs.JobMetrics, @@ -59,7 +54,6 @@ func NewSyncWorker( clients: clients, repositoryResources: repositoryResources, patchStatus: patchStatus, - storageStatus: storageStatus, syncer: syncer, metrics: metrics, tracer: tracer, @@ -96,13 +90,6 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo ) }() - // Check if we are onboarding from legacy storage - // HACK -- this should be handled outside of this worker - if r.storageStatus != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, r.storageStatus) { - err := fmt.Errorf("sync not supported until storage has migrated") - return tracing.Error(span, err) - } - rw, ok := repo.(repository.ReaderWriter) if !ok { err := fmt.Errorf("sync job submitted for repository that does not support read-write") diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go index a5013539957..34d32372936 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go @@ -10,7 +10,6 @@ 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/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -46,7 +45,7 @@ func TestSyncWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewSyncWorker(nil, nil, nil, nil, nil, metrics, tracing.NewNoopTracerService(), 10) + worker := NewSyncWorker(nil, nil, nil, nil, metrics, tracing.NewNoopTracerService(), 10) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -63,9 +62,7 @@ func TestSyncWorker_ProcessNotReaderWriter(t *testing.T) { Title: "test-repo", }, }) - fakeDualwrite := dualwrite.NewMockService(t) - fakeDualwrite.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - worker := NewSyncWorker(nil, nil, fakeDualwrite, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), tracing.NewNoopTracerService(), 10) + worker := NewSyncWorker(nil, nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), tracing.NewNoopTracerService(), 10) err := worker.Process(context.Background(), repo, provisioning.Job{}, jobs.NewMockJobProgressRecorder(t)) require.EqualError(t, err, "sync job submitted for repository that does not support read-write") } @@ -73,31 +70,13 @@ func TestSyncWorker_ProcessNotReaderWriter(t *testing.T) { func TestSyncWorker_Process(t *testing.T) { tests := []struct { name string - setupMocks func(*resources.MockClientFactory, *resources.MockRepositoryResourcesFactory, *dualwrite.MockService, *MockRepositoryPatchFn, *MockSyncer, *mockReaderWriter, *jobs.MockJobProgressRecorder) + setupMocks func(*resources.MockClientFactory, *resources.MockRepositoryResourcesFactory, *MockRepositoryPatchFn, *MockSyncer, *mockReaderWriter, *jobs.MockJobProgressRecorder) expectedError string expectedStatus *provisioning.SyncStatus }{ - { - name: "legacy storage not migrated", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { - rw.MockRepository.On("Config").Return(&provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Spec: provisioning.RepositorySpec{ - Title: "test-repo", - }, - }) - - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil).Twice() - }, - expectedError: "sync not supported until storage has migrated", - }, { name: "failed initial status patching", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { // Setup repository config with existing LastRef repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ @@ -132,7 +111,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "failed getting repository resources", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { // Setup repository config repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ @@ -149,9 +128,6 @@ func TestSyncWorker_Process(t *testing.T) { } rw.MockRepository.On("Config").Return(repoConfig) - // Storage is migrated - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // 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, mock.Anything, mock.Anything).Return(nil).Once() @@ -168,7 +144,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "failed getting clients for namespace", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { // Setup repository config repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ @@ -186,9 +162,6 @@ func TestSyncWorker_Process(t *testing.T) { } rw.MockRepository.On("Config").Return(repoConfig) - // Storage is migrated - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // 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, mock.Anything, mock.Anything).Return(nil).Once() @@ -208,7 +181,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "successful sync", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -222,9 +195,6 @@ func TestSyncWorker_Process(t *testing.T) { } rw.MockRepository.On("Config").Return(repoConfig) - // Storage is migrated - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // 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, mock.Anything, mock.Anything).Return(nil).Once() @@ -261,7 +231,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "failed sync", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -275,9 +245,6 @@ func TestSyncWorker_Process(t *testing.T) { } rw.MockRepository.On("Config").Return(repoConfig) - // Storage is migrated - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // 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, mock.Anything, mock.Anything).Return(nil).Once() @@ -315,7 +282,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "stats call fails", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -323,7 +290,6 @@ func TestSyncWorker_Process(t *testing.T) { }, } rw.MockRepository.On("Config").Return(repoConfig) - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() mockRepoResources := resources.NewMockRepositoryResources(t) mockRepoResources.On("Stats", mock.Anything).Return(nil, errors.New("stats error")) @@ -344,7 +310,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "stats returns nil stats and nil error", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -352,7 +318,6 @@ func TestSyncWorker_Process(t *testing.T) { }, } rw.MockRepository.On("Config").Return(repoConfig) - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() mockRepoResources := resources.NewMockRepositoryResources(t) mockRepoResources.On("Stats", mock.Anything).Return(nil, nil) @@ -378,7 +343,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "stats returns one managed stats", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -386,7 +351,6 @@ func TestSyncWorker_Process(t *testing.T) { }, } rw.MockRepository.On("Config").Return(repoConfig) - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() // Initial patch with granular updates rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() @@ -439,7 +403,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "stats returns multiple managed stats", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -447,7 +411,6 @@ func TestSyncWorker_Process(t *testing.T) { }, } rw.MockRepository.On("Config").Return(repoConfig) - ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() mockRepoResources := resources.NewMockRepositoryResources(t) stats := &provisioning.ResourceStats{ @@ -495,7 +458,7 @@ func TestSyncWorker_Process(t *testing.T) { }, { name: "failed final status patch", - setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, ds *dualwrite.MockService, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { + setupMocks: func(cf *resources.MockClientFactory, rrf *resources.MockRepositoryResourcesFactory, rpf *MockRepositoryPatchFn, s *MockSyncer, rw *mockReaderWriter, pr *jobs.MockJobProgressRecorder) { repoConfig := &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -503,7 +466,6 @@ 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 - expect granular patches rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() @@ -534,7 +496,6 @@ func TestSyncWorker_Process(t *testing.T) { // Create mocks clientFactory := resources.NewMockClientFactory(t) repoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t) - dualwriteService := dualwrite.NewMockService(t) repositoryPatchFn := NewMockRepositoryPatchFn(t) syncer := NewMockSyncer(t) readerWriter := &mockReaderWriter{ @@ -544,13 +505,12 @@ func TestSyncWorker_Process(t *testing.T) { progressRecorder := jobs.NewMockJobProgressRecorder(t) // Setup mocks - tt.setupMocks(clientFactory, repoResourcesFactory, dualwriteService, repositoryPatchFn, syncer, readerWriter, progressRecorder) + tt.setupMocks(clientFactory, repoResourcesFactory, repositoryPatchFn, syncer, readerWriter, progressRecorder) // Create worker worker := NewSyncWorker( clientFactory, repoResourcesFactory, - dualwriteService, repositoryPatchFn.Execute, syncer, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), diff --git a/pkg/registry/apis/provisioning/jobs/worker_mock.go b/pkg/registry/apis/provisioning/jobs/worker_mock.go index 52f0683fdd8..d669193c166 100644 --- a/pkg/registry/apis/provisioning/jobs/worker_mock.go +++ b/pkg/registry/apis/provisioning/jobs/worker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index d18fc1156a8..26e97f56b3c 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -28,9 +28,9 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - 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/auth" + connectionvalidation "github.com/grafana/grafana/apps/provisioning/pkg/connection" appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller" 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" @@ -53,14 +53,12 @@ import ( movepkg "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/move" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" "github.com/grafana/grafana/pkg/registry/apis/provisioning/usage" "github.com/grafana/grafana/pkg/services/apiserver" "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/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -111,11 +109,13 @@ type APIBuilder struct { jobHistoryLoki *jobs.LokiJobHistory resourceLister resources.ResourceLister dashboardAccess legacy.MigrationDashboardAccessor - storageStatus dualwrite.Service unified resource.ResourceClient repoFactory repository.Factory client client.ProvisioningV0alpha1Interface - access authlib.AccessChecker + access auth.AccessChecker + accessWithAdmin auth.AccessChecker + accessWithEditor auth.AccessChecker + accessWithViewer auth.AccessChecker statusPatcher *appcontroller.RepositoryStatusPatcher healthChecker *controller.HealthChecker validator repository.RepositoryValidator @@ -158,9 +158,17 @@ func NewAPIBuilder( } else { clients = resources.NewClientFactory(configProvider) } + parsers := resources.NewParserFactory(clients) - legacyMigrator := migrations.ProvideUnifiedMigrator(dashboardAccess, unified) - resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) + resourceLister := resources.NewResourceListerForMigrations(unified) + + // Create access checker based on mode + var accessChecker auth.AccessChecker + if useExclusivelyAccessCheckerForAuthz { + accessChecker = auth.NewTokenAccessChecker(access) + } else { + accessChecker = auth.NewSessionAccessChecker(access) + } b := &APIBuilder{ onlyApiServer: onlyApiServer, @@ -173,9 +181,11 @@ func NewAPIBuilder( repositoryResources: resources.NewRepositoryResourcesFactory(parsers, clients, resourceLister), resourceLister: resourceLister, dashboardAccess: dashboardAccess, - storageStatus: storageStatus, unified: unified, - access: access, + access: accessChecker, + accessWithAdmin: accessChecker.WithFallbackRole(identity.RoleAdmin), + accessWithEditor: accessChecker.WithFallbackRole(identity.RoleEditor), + accessWithViewer: accessChecker.WithFallbackRole(identity.RoleViewer), jobHistoryConfig: jobHistoryConfig, extraWorkers: extraWorkers, restConfigGetter: restConfigGetter, @@ -249,6 +259,10 @@ func RegisterAPIService( return nil, nil } + if dualwrite.IsReadingLegacyDashboardsAndFolders(context.Background(), storageStatus) { + return nil, fmt.Errorf("resources are stored in an incompatible data format to use provisioning. Please enable data migration in settings for folders and dashboards by adding the following configuration:\n[unified_storage.folders.folder.grafana.app]\nenableMigration = true\n\n[unified_storage.dashboards.dashboard.grafana.app]\nenableMigration = true\n\nAlternatively, disable provisioning") + } + allowedTargets := []provisioning.SyncTargetType{} for _, target := range cfg.ProvisioningAllowedTargets { allowedTargets = append(allowedTargets, provisioning.SyncTargetType(target)) @@ -299,120 +313,210 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { } } - info, ok := authlib.AuthInfoFrom(ctx) - // 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(), - Resource: a.GetResource(), - Name: a.GetName(), - Namespace: a.GetNamespace(), - Subresource: a.GetSubresource(), - Path: a.GetPath(), - }, "") - if err != nil { - return authorizer.DecisionDeny, "failed to perform authorization", err - } - - if !res.Allowed { - return authorizer.DecisionDeny, "permission denied", nil - } - - return authorizer.DecisionAllow, "", nil - } - - id, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "failed to find requester", err - } - - // Different routes may need different permissions. - // * Reading and modifying a repository's configuration requires administrator privileges. - // * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges. - // * Reading a repository's files requires viewer privileges. - // * Reading a repository's refs requires viewer privileges. - // * Editing a repository's files requires editor privileges. - // * Syncing a repository requires editor privileges. - // * Exporting a repository requires administrator privileges. - // * Migrating a repository requires administrator privileges. - // * Testing a repository configuration requires administrator privileges. - // * Viewing a repository's history requires editor privileges. - - switch a.GetResource() { - case provisioning.RepositoryResourceInfo.GetName(): - // TODO: Support more fine-grained permissions than the basic roles. Especially on Enterprise. - switch a.GetSubresource() { - case "", "test", "jobs": - // Doing something with the repository itself. - if id.GetOrgRole().Includes(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "admin role is required", nil - - case "refs": - // This is strictly a read operation. It is handy on the frontend for viewers. - if id.GetOrgRole().Includes(identity.RoleViewer) { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "viewer role is required", nil - case "files": - // Access to files is controlled by the AccessClient - return authorizer.DecisionAllow, "", nil - - case "resources", "sync", "history": - // These are strictly read operations. - // Sync can also be somewhat destructive, but it's expected to be fine to import changes. - if id.GetOrgRole().Includes(identity.RoleEditor) { - return authorizer.DecisionAllow, "", nil - } else { - return authorizer.DecisionDeny, "editor role is required", nil - } - case "status": - if id.GetOrgRole().Includes(identity.RoleViewer) && a.GetVerb() == apiutils.VerbGet { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "users cannot update the status of a repository", nil - default: - if id.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil - } - - case "stats": - // This can leak information one shouldn't necessarily have access to. - if id.GetOrgRole().Includes(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "admin role is required", nil - - case "settings": - // This is strictly a read operation. It is handy on the frontend for viewers. - if id.GetOrgRole().Includes(identity.RoleViewer) { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "viewer role is required", nil - - case provisioning.JobResourceInfo.GetName(), - provisioning.HistoricJobResourceInfo.GetName(): - // Jobs are shown on the configuration page. - if id.GetOrgRole().Includes(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "admin role is required", nil - - default: - // We haven't bothered with this kind yet. - if id.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "unmapped kind defaults to no access", nil - } + return b.authorizeResource(ctx, a) }) } +// authorizeResource handles authorization for different resources. +// Uses fine-grained permissions defined in accesscontrol.go: +// +// Repositories: +// - CRUD: repositories:create/read/write/delete +// - Subresources: files (any auth), refs (editor), resources/history/status (admin) +// - Test: repositories:write +// - Jobs subresource: jobs:create/read +// +// Connections: +// - CRUD: connections:create/read/write/delete +// - Status: connections:read +// +// Jobs: +// - CRUD: jobs:create/read/write/delete +// +// Historic Jobs: +// - Read-only: historicjobs:read +// +// Settings: +// - settings:read - granted to Viewer (all logged-in users) +// +// Stats: +// - stats:read - granted to Admin only +func (b *APIBuilder) authorizeResource(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + switch a.GetResource() { + case provisioning.RepositoryResourceInfo.GetName(): + return b.authorizeRepositorySubresource(ctx, a) + case provisioning.ConnectionResourceInfo.GetName(): + return b.authorizeConnectionSubresource(ctx, a) + case provisioning.JobResourceInfo.GetName(): + return toAuthorizerDecision(b.accessWithEditor.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: provisioning.JobResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + case provisioning.HistoricJobResourceInfo.GetName(): + // Historic jobs are read-only and admin-only (not editor) + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: provisioning.HistoricJobResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + case "settings": + // Settings are read-only and accessible by all logged-in users (Viewer role) + return toAuthorizerDecision(b.accessWithViewer.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: "settings", + Namespace: a.GetNamespace(), + }, "")) + case "stats": + // Stats are read-only and admin-only + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: "stats", + Namespace: a.GetNamespace(), + }, "")) + default: + return b.authorizeDefault(ctx) + } +} + +// authorizeRepositorySubresource handles authorization for repository subresources. +// Uses the access checker with verb-based authorization. +func (b *APIBuilder) authorizeRepositorySubresource(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + switch a.GetSubresource() { + // Repository CRUD - use access checker with the actual verb + case "": + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + // Test requires write permission (testing before save) + case "test": + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: apiutils.VerbUpdate, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + // Files subresource: allow any authenticated user at route level. + // Directory listing checks repositories:read in the connector. + // Individual file operations are authorized by DualReadWriter based on the actual resource. + case "files": + return authorizer.DecisionAllow, "", nil + + // refs subresource - editors need to see branches to push changes + case "refs": + return toAuthorizerDecision(b.accessWithEditor.Check(ctx, authlib.CheckRequest{ + Verb: apiutils.VerbGet, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + // Read-only subresources: resources, history, status (admin only) + case "resources", "history", "status": + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: apiutils.VerbGet, + Group: provisioning.GROUP, + Resource: provisioning.RepositoryResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + // Jobs subresource - check jobs permissions with the verb (editors can manage jobs) + case "jobs": + return toAuthorizerDecision(b.accessWithEditor.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: provisioning.JobResourceInfo.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + default: + id, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "failed to find requester", err + } + if id.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil + } +} + +// authorizeConnectionSubresource handles authorization for connection subresources. +// Uses the access checker with verb-based authorization. +func (b *APIBuilder) authorizeConnectionSubresource(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + switch a.GetSubresource() { + // Connection CRUD - use access checker with the actual verb + case "": + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: a.GetVerb(), + Group: provisioning.GROUP, + Resource: provisioning.ConnectionResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + // Status is read-only + case "status": + return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{ + Verb: apiutils.VerbGet, + Group: provisioning.GROUP, + Resource: provisioning.ConnectionResourceInfo.GetName(), + Name: a.GetName(), + Namespace: a.GetNamespace(), + }, "")) + + default: + id, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "failed to find requester", err + } + if id.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil + } +} + +// ---------------------------------------------------------------------------- +// Authorization helpers +// ---------------------------------------------------------------------------- + +// toAuthorizerDecision converts an access check error to an authorizer decision tuple. +func toAuthorizerDecision(err error) (authorizer.Decision, string, error) { + if err != nil { + return authorizer.DecisionDeny, err.Error(), nil + } + return authorizer.DecisionAllow, "", nil +} + +// authorizeDefault handles authorization for unmapped resources. +func (b *APIBuilder) authorizeDefault(ctx context.Context) (authorizer.Decision, string, error) { + id, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "failed to find requester", err + } + // We haven't bothered with this kind yet. + if id.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "unmapped kind defaults to no access", nil +} + func (b *APIBuilder) GetGroupVersion() schema.GroupVersion { return provisioning.SchemeGroupVersion } @@ -487,13 +591,22 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI storage[provisioning.HistoricJobResourceInfo.StoragePath()] = historicJobStore } + connectionsStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, provisioning.ConnectionResourceInfo, opts.OptsGetter) + if err != nil { + return fmt.Errorf("failed to create connection storage: %w", err) + } + connectionStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, connectionsStore) + storage[provisioning.JobResourceInfo.StoragePath()] = jobStore storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage + storage[provisioning.ConnectionResourceInfo.StoragePath()] = connectionsStore + storage[provisioning.ConnectionResourceInfo.StoragePath("status")] = connectionStatusStorage + // TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories)) - storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access) + storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.accessWithAdmin) storage[provisioning.RepositoryResourceInfo.StoragePath("refs")] = NewRefsConnector(b) storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{ getter: b, @@ -533,6 +646,11 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis if ok { return nil } + // TODO: complete this as part of https://github.com/grafana/git-ui-sync-project/issues/700 + c, ok := obj.(*provisioning.Connection) + if ok { + return connectionvalidation.MutateConnection(c) + } r, ok := obj.(*provisioning.Repository) if !ok { @@ -582,6 +700,11 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm return nil } + connection, ok := obj.(*provisioning.Connection) + if ok { + return connectionvalidation.ValidateConnection(connection) + } + // Validate Jobs job, ok := obj.(*provisioning.Job) if ok { @@ -598,7 +721,8 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm // // the only time to add configuration checks here is if you need to compare // the incoming change to the current configuration - list := b.validator.ValidateRepository(repo) + isCreate := a.GetOperation() == admission.Create + list := b.validator.ValidateRepository(repo, isCreate) cfg := repo.Config() if a.GetOperation() == admission.Update { @@ -685,12 +809,6 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH go repoInformer.Informer().Run(postStartHookCtx.Done()) go jobInformer.Informer().Run(postStartHookCtx.Done()) - // When starting with an empty instance -- swith to "mode 4+" - err = b.tryRunningOnlyUnifiedStorage() - if err != nil { - return err - } - // Create the repository resources factory repositoryListerWrapper := func(ctx context.Context) ([]provisioning.Repository, error) { return GetRepositoriesInNamespace(ctx, b.store) @@ -713,29 +831,12 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH syncWorker := sync.NewSyncWorker( b.clients, b.repositoryResources, - b.storageStatus, b.statusPatcher.Patch, syncer, metrics, b.tracer, 10, ) - signerFactory := signature.NewSignerFactory(b.clients) - legacyResources := migrate.NewLegacyResourcesMigrator( - b.repositoryResources, - b.parsers, - b.dashboardAccess, - signerFactory, - b.clients, - export.ExportAll, - ) - storageSwapper := migrate.NewStorageSwapper(b.unified, b.storageStatus) - legacyMigrator := migrate.NewLegacyMigrator( - legacyResources, - storageSwapper, - syncWorker, - stageIfPossible, - ) cleaner := migrate.NewNamespaceCleaner(b.clients) unifiedStorageMigrator := migrate.NewUnifiedStorageMigrator( @@ -743,12 +844,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH exportWorker, syncWorker, ) - - migrationWorker := migrate.NewMigrationWorker( - legacyMigrator, - unifiedStorageMigrator, - b.storageStatus, - ) + migrationWorker := migrate.NewMigrationWorker(unifiedStorageMigrator) deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources, metrics) moveWorker := movepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources, metrics) @@ -820,7 +916,6 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.resourceLister, b.clients, b.jobs, - b.storageStatus, b.GetHealthChecker(), b.statusPatcher, b.registry, @@ -1229,65 +1324,6 @@ spec: return oas, nil } -// FIXME: This logic does not belong in provisioning! (but required for now) -// When starting an empty instance, we shift so that we never reference legacy storage -// This should run somewhere else at startup by default (dual writer? dashboards?) -func (b *APIBuilder) tryRunningOnlyUnifiedStorage() error { - ctx := context.Background() - - if !b.storageStatus.ShouldManage(dashboard.DashboardResourceInfo.GroupResource()) { - return nil // not enabled - } - - if !dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus) { - return nil - } - - // Count how many things exist - create a migrator on-demand for this - legacyMigrator := migrations.ProvideUnifiedMigrator(b.dashboardAccess, b.unified) - rsp, err := legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ - Namespace: "default", // FIXME! this works for single org, but need to check multi-org - Resources: []schema.GroupResource{{ - Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, - }, { - Group: folders.GROUP, Resource: folders.RESOURCE, - }}, - OnlyCount: true, - }) - if err != nil { - return fmt.Errorf("error getting legacy count %w", err) - } - for _, stats := range rsp.Summary { - if stats.Count > 0 { - return nil // something exists we can not just switch - } - } - - logger := logging.DefaultLogger.With("logger", "provisioning startup") - mode5 := func(gr schema.GroupResource) error { - status, _ := b.storageStatus.Status(ctx, gr) - if !status.ReadUnified { - status.ReadUnified = true - status.WriteLegacy = false - status.WriteUnified = true - status.Runtime = false - status.Migrated = time.Now().UnixMilli() - _, err = b.storageStatus.Update(ctx, status) - logger.Info("set unified storage access", "group", gr.Group, "resource", gr.Resource) - return err - } - return nil // already reading unified - } - - if err = mode5(dashboard.DashboardResourceInfo.GroupResource()); err != nil { - return err - } - if err = mode5(folders.FolderResourceInfo.GroupResource()); err != nil { - return err - } - return nil -} - // Helpers for fetching valid Repository objects // TODO: where should the helpers live? diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index 62f4ffd3b98..e8a7ee83dd0 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -3,6 +3,7 @@ package resources import ( "context" "fmt" + "net/http" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -11,6 +12,7 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/auth" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" @@ -31,7 +33,7 @@ type DualReadWriter struct { repo repository.ReaderWriter parser Parser folders *FolderManager - access authlib.AccessChecker + access auth.AccessChecker } type DualWriteOptions struct { @@ -47,7 +49,7 @@ type DualWriteOptions struct { Branch string // Configured default branch } -func NewDualReadWriter(repo repository.ReaderWriter, parser Parser, folders *FolderManager, access authlib.AccessChecker) *DualReadWriter { +func NewDualReadWriter(repo repository.ReaderWriter, parser Parser, folders *FolderManager, access auth.AccessChecker) *DualReadWriter { return &DualReadWriter{repo: repo, parser: parser, folders: folders, access: access} } @@ -276,6 +278,16 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D // FIXME: to make sure if behaves in the same way as in sync, we should // we should refactor the code to use the same function. if r.shouldUpdateGrafanaDB(opts, parsed) { + // HACK: Get the has from repository -- this will avoid an additional RV increment + // we should change the signature of Create and Update to return FileInfo instead + info, _ = r.repo.Read(ctx, opts.Path, opts.Ref) + if info != nil { + parsed.Meta.SetSourceProperties(utils.SourceProperties{ + Path: opts.Path, + Checksum: info.Hash, + }) + } + if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil { return nil, fmt.Errorf("ensure folder path exists: %w", err) } @@ -315,7 +327,19 @@ func (r *DualReadWriter) MoveResource(ctx context.Context, opts DualWriteOptions } func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { - // For directory moves, we just perform the repository move without parsing + // Reject directory move operations for configured branch - use bulk operations instead + if r.isConfiguredBranch(opts) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusMethodNotAllowed, + Reason: metav1.StatusReasonMethodNotAllowed, + Message: "directory move operations are not available for configured branch. Use bulk move operations via the jobs API instead", + }, + } + } + + // For branch operations, we just perform the repository move without updating Grafana DB // Always use the provisioning identity when writing ctx, _, err := identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace) if err != nil { @@ -349,35 +373,6 @@ func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOption }, } - // Handle folder management for main branch - if r.shouldUpdateGrafanaDB(opts, nil) { - // Ensure destination folder path exists - if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil { - return nil, fmt.Errorf("ensure destination folder path exists: %w", err) - } - - // Try to delete the old folder structure from grafana (if it exists) - // This handles cleanup when folders are moved to new locations - oldFolderName, err := r.folders.EnsureFolderPathExist(ctx, opts.OriginalPath) - if err != nil { - return nil, fmt.Errorf("ensure original folder path exists: %w", err) - } - - if oldFolderName != "" { - oldFolder, err := r.folders.GetFolder(ctx, oldFolderName) - if err != nil && !apierrors.IsNotFound(err) { - return nil, fmt.Errorf("get old folder for cleanup: %w", err) - } - - if err == nil { - err = r.folders.Client().Delete(ctx, oldFolder.GetName(), metav1.DeleteOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return nil, fmt.Errorf("delete old folder from storage: %w", err) - } - } - } - } - return parsed, nil } @@ -498,11 +493,6 @@ func (r *DualReadWriter) moveFile(ctx context.Context, opts DualWriteOptions) (* } func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error { - id, err := identity.GetRequester(ctx) - if err != nil { - return apierrors.NewUnauthorized(err.Error()) - } - var name string if parsed.Existing != nil { name = parsed.Existing.GetName() @@ -510,82 +500,52 @@ func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, name = parsed.Obj.GetName() } - rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{ - Group: parsed.GVR.Group, - Resource: parsed.GVR.Resource, - Namespace: id.GetNamespace(), - Name: name, - Verb: verb, + return r.access.Check(ctx, authlib.CheckRequest{ + Group: parsed.GVR.Group, + Resource: parsed.GVR.Resource, + Name: name, + Verb: verb, }, parsed.Meta.GetFolder()) - if err != nil || !rsp.Allowed { - return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), - fmt.Errorf("no access to read the embedded file")) - } - - idType, _, err := authlib.ParseTypeID(id.GetID()) - if err != nil { - return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), fmt.Errorf("could not determine identity type to check access")) - } - // only apply role based access if identity is not of type access policy - if idType == authlib.TypeAccessPolicy || id.GetOrgRole().Includes(identity.RoleEditor) { - return nil - } - - return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), - fmt.Errorf("must be admin or editor to access files from provisioning")) } -func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) error { - id, err := identity.GetRequester(ctx) - if err != nil { - return apierrors.NewUnauthorized(err.Error()) +func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, path string) error { + // Determine parent folder from path + parentFolder := "" + if path != "" { + parentPath := safepath.Dir(path) + if parentPath != "" { + parentFolder = ParseFolder(parentPath, r.repo.Config().Name).ID + } else { + parentFolder = RootFolder(r.repo.Config()) + } } - // Simple role based access for now - if id.GetOrgRole().Includes(identity.RoleEditor) { - return nil - } - - return apierrors.NewForbidden(FolderResource.GroupResource(), "", - fmt.Errorf("must be admin or editor to access folders with provisioning")) + // For folder create operations, use empty name to check parent folder permissions + return r.access.Check(ctx, authlib.CheckRequest{ + Group: FolderResource.Group, + Resource: FolderResource.Resource, + Name: "", // Empty name for create operations + Verb: utils.VerbCreate, + }, parentFolder) } func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { - // if the ref is set, it is not the active branch, so just delete the files from the branch - // and do not delete the items from grafana itself - if !r.shouldUpdateGrafanaDB(opts, nil) { - err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) - if err != nil { - return nil, fmt.Errorf("error deleting folder from repository: %w", err) + // Reject directory delete operations for configured branch - use bulk operations instead + if r.isConfiguredBranch(opts) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusMethodNotAllowed, + Reason: metav1.StatusReasonMethodNotAllowed, + Message: "directory delete operations are not available for configured branch. Use bulk delete operations via the jobs API instead", + }, } - - return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) } - // before deleting from the repo, first get all children resources to delete from grafana afterwards - treeEntries, err := r.repo.ReadTree(ctx, "") + // For branch operations, just delete from the repository without updating Grafana DB + err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) if err != nil { - return nil, fmt.Errorf("read repository tree: %w", err) - } - // note: parsedFolders will include the folder itself - parsedResources, parsedFolders, err := r.getChildren(ctx, opts.Path, treeEntries) - if err != nil { - return nil, fmt.Errorf("parse resources in folder: %w", err) - } - - // delete from the repo - err = r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) - if err != nil { - return nil, fmt.Errorf("delete folder from repository: %w", err) - } - - // delete from grafana - ctx, _, err = identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace) - if err != nil { - return nil, err - } - if err := r.deleteChildren(ctx, parsedResources, parsedFolders); err != nil { - return nil, fmt.Errorf("delete folder from grafana: %w", err) + return nil, fmt.Errorf("error deleting folder from repository: %w", err) } return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) @@ -640,60 +600,11 @@ func folderDeleteResponse(ctx context.Context, path, ref string, repo repository return parsed, nil } -func (r *DualReadWriter) getChildren(ctx context.Context, folderPath string, treeEntries []repository.FileTreeEntry) ([]*ParsedResource, []Folder, error) { - var resourcesInFolder []repository.FileTreeEntry - var foldersInFolder []Folder - for _, entry := range treeEntries { - // make sure the path is supported (i.e. not ignored by git sync) and that the path is the folder itself or a child of the folder - if IsPathSupported(entry.Path) != nil || !safepath.InDir(entry.Path, folderPath) { - continue - } - // folders cannot be parsed as resources, so handle them separately - if entry.Blob { - resourcesInFolder = append(resourcesInFolder, entry) - } else { - folder := ParseFolder(entry.Path, r.repo.Config().Name) - foldersInFolder = append(foldersInFolder, folder) - } - } - - parsedResources := make([]*ParsedResource, len(resourcesInFolder)) - for i, entry := range resourcesInFolder { - fileInfo, err := r.repo.Read(ctx, entry.Path, "") - if err != nil && !apierrors.IsNotFound(err) { - return nil, nil, fmt.Errorf("could not find resource in repository: %w", err) - } - - parsed, err := r.parser.Parse(ctx, fileInfo) - if err != nil { - return nil, nil, fmt.Errorf("could not parse resource: %w", err) - } - - parsedResources[i] = parsed - } - - return parsedResources, foldersInFolder, nil -} - -func (r *DualReadWriter) deleteChildren(ctx context.Context, childrenResources []*ParsedResource, folders []Folder) error { - for _, parsed := range childrenResources { - err := parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return fmt.Errorf("failed to delete nested resource from grafana: %w", err) - } - } - - // we need to delete the folders furthest down in the tree first, as folder deletion will fail if there is anything inside of it - safepath.SortByDepth(folders, func(f Folder) string { return f.Path }, false) - - for _, f := range folders { - err := r.folders.Client().Delete(ctx, f.ID, metav1.DeleteOptions{}) - if err != nil { - return fmt.Errorf("failed to delete folder from grafana: %w", err) - } - } - - return nil +// isConfiguredBranch returns true if the ref targets the configured branch +// (empty ref means configured branch, or ref explicitly matches configured branch) +func (r *DualReadWriter) isConfiguredBranch(opts DualWriteOptions) bool { + configuredBranch := r.repo.Config().Branch() + return opts.Ref == "" || opts.Ref == configuredBranch } // shouldUpdateGrafanaDB returns true if we have an empty ref (targeting the configured branch) @@ -703,9 +614,5 @@ func (r *DualReadWriter) shouldUpdateGrafanaDB(opts DualWriteOptions, parsed *Pa return false } - if opts.Ref != "" && opts.Ref != opts.Branch { - return false - } - - return true + return r.isConfiguredBranch(opts) } diff --git a/pkg/registry/apis/provisioning/resources/folders.go b/pkg/registry/apis/provisioning/resources/folders.go index b74ab67976a..8b8f4201745 100644 --- a/pkg/registry/apis/provisioning/resources/folders.go +++ b/pkg/registry/apis/provisioning/resources/folders.go @@ -197,6 +197,8 @@ func (fm *FolderManager) EnsureFolderTreeExists(ctx context.Context, ref, path s if err != nil && (!errors.Is(err, repository.ErrFileNotFound) && !apierrors.IsNotFound(err)) { return fn(folder, false, fmt.Errorf("check if folder exists before writing: %w", err)) } else if err == nil { + // Folder already exists in repository, add it to tree so resources can find it + fm.tree.Add(folder, parent) return fn(folder, false, nil) } diff --git a/pkg/registry/apis/provisioning/resources/object.go b/pkg/registry/apis/provisioning/resources/object.go index 0a016786410..a51217d08af 100644 --- a/pkg/registry/apis/provisioning/resources/object.go +++ b/pkg/registry/apis/provisioning/resources/object.go @@ -4,15 +4,9 @@ import ( "context" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - 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/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -31,25 +25,16 @@ type ResourceStore interface { } type ResourceListerFromSearch struct { - store ResourceStore - migrator migrations.UnifiedMigrator - storageStatus dualwrite.Service + store ResourceStore } func NewResourceLister(store ResourceStore) ResourceLister { return &ResourceListerFromSearch{store: store} } -// FIXME: the logic about migration and storage should probably be separated from this -func NewResourceListerForMigrations( - store ResourceStore, - migrator migrations.UnifiedMigrator, - storageStatus dualwrite.Service, -) ResourceLister { +func NewResourceListerForMigrations(store ResourceStore) ResourceLister { return &ResourceListerFromSearch{ - store: store, - migrator: migrator, - storageStatus: storageStatus, + store: store, } } @@ -133,37 +118,6 @@ func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, reposit return stats, nil } - // Get the stats based on what a migration could support - if o.storageStatus != nil && o.migrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { - rsp, err := o.migrator.Migrate(ctx, legacy.MigrateOptions{ - Namespace: namespace, - Resources: []schema.GroupResource{{ - Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, - }, { - Group: folders.GROUP, Resource: folders.RESOURCE, - }}, - WithHistory: false, - OnlyCount: true, - }) - if err != nil { - return nil, err - } - for _, v := range rsp.Summary { - stats.Instance = append(stats.Instance, provisioning.ResourceCount{ - Group: v.Group, - Resource: v.Resource, - Count: v.Count, - }) - // Everything is unmanaged in legacy storage - stats.Unmanaged = append(stats.Unmanaged, provisioning.ResourceCount{ - Group: v.Group, - Resource: v.Resource, - Count: v.Count, - }) - } - return stats, nil - } - // Get full instance stats info, err := o.store.GetStats(ctx, &resourcepb.ResourceStatsRequest{ Namespace: namespace, diff --git a/pkg/registry/apis/provisioning/resources/resources.go b/pkg/registry/apis/provisioning/resources/resources.go index 4954e6b51b6..8e12c7556cd 100644 --- a/pkg/registry/apis/provisioning/resources/resources.go +++ b/pkg/registry/apis/provisioning/resources/resources.go @@ -180,7 +180,12 @@ func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj var ok bool fid, ok = r.folders.Tree().DirPath(folder, rootFolder) if !ok { - return "", fmt.Errorf("folder %s NOT found in tree with root: %s", folder, rootFolder) + // HACK: this is a hack to get the folder path without the root folder + // TODO: should we build the tree in a different way? + fid, ok = r.folders.Tree().DirPath(folder, "") + if !ok { + return "", fmt.Errorf("folder %s NOT found in tree", folder) + } } } diff --git a/pkg/registry/apis/provisioning/resources/signature/grafana.go b/pkg/registry/apis/provisioning/resources/signature/grafana.go deleted file mode 100644 index e91497c1245..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/grafana.go +++ /dev/null @@ -1,33 +0,0 @@ -package signature - -import ( - "context" - - "github.com/grafana/grafana/apps/provisioning/pkg/repository" - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -type grafanaSigner struct{} - -// FIXME: where should we use this default signature? -// NewGrafanaSigner returns a Signer that uses the grafana user as the author -func NewGrafanaSigner() Signer { - return &grafanaSigner{} -} - -func (s *grafanaSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) { - sig := repository.CommitSignature{ - Name: "grafana", - // TODO: should we add email? - // Email: "grafana@grafana.com", - } - - t, err := item.GetUpdatedTimestamp() - if err == nil && t != nil { - sig.When = *t - } else { - sig.When = item.GetCreationTimestamp().Time - } - - return repository.WithAuthorSignature(ctx, sig), nil -} diff --git a/pkg/registry/apis/provisioning/resources/signature/grafana_test.go b/pkg/registry/apis/provisioning/resources/signature/grafana_test.go deleted file mode 100644 index cae5fbf4ebb..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/grafana_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package signature - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/apps/provisioning/pkg/repository" - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -func TestNewGrafanaSigner(t *testing.T) { - signer := NewGrafanaSigner() - require.NotNil(t, signer, "signer should not be nil") - require.IsType(t, &grafanaSigner{}, signer, "signer should be of type *grafanaSigner") -} - -func TestGrafanaSigner_Sign(t *testing.T) { - tests := []struct { - name string - creationTimestamp time.Time - updateTimestampErr error - updatedTimestamp *time.Time - expectedTime time.Time - setupMocks func(meta *utils.MockGrafanaMetaAccessor) - }{ - { - name: "should use creation timestamp when no update timestamp", - creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - updatedTimestamp: ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), - updateTimestampErr: errors.New("failed"), - expectedTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - setupMocks: func(meta *utils.MockGrafanaMetaAccessor) { - meta.On("GetCreationTimestamp").Return(metav1.Time{Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}) - }, - }, - { - name: "should use creation timestamp when update timestamp is nil", - creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - updatedTimestamp: nil, - expectedTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - setupMocks: func(meta *utils.MockGrafanaMetaAccessor) { - meta.On("GetCreationTimestamp").Return(metav1.Time{Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}) - }, - }, - { - name: "should use update timestamp when available", - creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - updatedTimestamp: ptr(time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)), - expectedTime: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - meta := utils.NewMockGrafanaMetaAccessor(t) - var updatedTime *time.Time - if tt.updatedTimestamp != nil { - updatedTime = tt.updatedTimestamp - } - meta.On("GetUpdatedTimestamp").Return(updatedTime, tt.updateTimestampErr) - - if tt.setupMocks != nil { - tt.setupMocks(meta) - } - - signer := NewGrafanaSigner() - ctx := context.Background() - - signedCtx, err := signer.Sign(ctx, meta) - require.NoError(t, err) - - // Verify the signature in the context - sig := repository.GetAuthorSignature(signedCtx) - require.NotNil(t, sig, "signature should be present in context") - require.Equal(t, "grafana", sig.Name) - require.Equal(t, tt.expectedTime, sig.When) - - meta.AssertExpectations(t) - }) - } -} - -func ptr[T any](v T) *T { - return &v -} diff --git a/pkg/registry/apis/provisioning/resources/signature/signature.go b/pkg/registry/apis/provisioning/resources/signature/signature.go deleted file mode 100644 index c0b39f7b325..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/signature.go +++ /dev/null @@ -1,52 +0,0 @@ -package signature - -import ( - "context" - "fmt" - - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" -) - -//go:generate mockery --name Signer --structname MockSigner --inpackage --filename signer_mock.go --with-expecter -type Signer interface { - Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) -} - -type SignOptions struct { - Namespace string - History bool -} - -// SignerFactory is a factory for creating Signers -// -//go:generate mockery --name SignerFactory --structname MockSignerFactory --inpackage --filename signature_factory_mock.go --with-expecter -type SignerFactory interface { - New(ctx context.Context, opts SignOptions) (Signer, error) -} - -type signerFactory struct { - clients resources.ClientFactory -} - -func NewSignerFactory(clients resources.ClientFactory) SignerFactory { - return &signerFactory{clients} -} - -func (f *signerFactory) New(ctx context.Context, opts SignOptions) (Signer, error) { - if !opts.History { - return NewGrafanaSigner(), nil - } - - clients, err := f.clients.Clients(ctx, opts.Namespace) - if err != nil { - return nil, fmt.Errorf("get clients: %w", err) - } - - userClient, err := clients.User(ctx) - if err != nil { - return nil, fmt.Errorf("get user client: %w", err) - } - - return NewLoadUsersOnceSigner(userClient), nil -} diff --git a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go b/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go deleted file mode 100644 index 80647e0129c..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package signature - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockSignerFactory is an autogenerated mock type for the SignerFactory type -type MockSignerFactory struct { - mock.Mock -} - -type MockSignerFactory_Expecter struct { - mock *mock.Mock -} - -func (_m *MockSignerFactory) EXPECT() *MockSignerFactory_Expecter { - return &MockSignerFactory_Expecter{mock: &_m.Mock} -} - -// New provides a mock function with given fields: ctx, opts -func (_m *MockSignerFactory) New(ctx context.Context, opts SignOptions) (Signer, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for New") - } - - var r0 Signer - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, SignOptions) (Signer, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, SignOptions) Signer); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(Signer) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, SignOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockSignerFactory_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New' -type MockSignerFactory_New_Call struct { - *mock.Call -} - -// New is a helper method to define mock.On call -// - ctx context.Context -// - opts SignOptions -func (_e *MockSignerFactory_Expecter) New(ctx interface{}, opts interface{}) *MockSignerFactory_New_Call { - return &MockSignerFactory_New_Call{Call: _e.mock.On("New", ctx, opts)} -} - -func (_c *MockSignerFactory_New_Call) Run(run func(ctx context.Context, opts SignOptions)) *MockSignerFactory_New_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(SignOptions)) - }) - return _c -} - -func (_c *MockSignerFactory_New_Call) Return(_a0 Signer, _a1 error) *MockSignerFactory_New_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockSignerFactory_New_Call) RunAndReturn(run func(context.Context, SignOptions) (Signer, error)) *MockSignerFactory_New_Call { - _c.Call.Return(run) - return _c -} - -// NewMockSignerFactory creates a new instance of MockSignerFactory. 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 NewMockSignerFactory(t interface { - mock.TestingT - Cleanup(func()) -}) *MockSignerFactory { - mock := &MockSignerFactory{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/resources/signature/signature_test.go b/pkg/registry/apis/provisioning/resources/signature/signature_test.go deleted file mode 100644 index b339865ca5f..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/signature_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package signature - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" -) - -func TestSignerFactory_New(t *testing.T) { - tests := []struct { - name string - opts SignOptions - setupMocks func(t *testing.T, clients *resources.MockClientFactory) - expectedType interface{} - expectedError string - }{ - { - name: "should return grafana signer when history is false", - opts: SignOptions{ - History: false, - }, - setupMocks: func(t *testing.T, clients *resources.MockClientFactory) { - // No mocks needed as we shouldn't call any clients - }, - expectedType: &grafanaSigner{}, - }, - { - name: "should return load users once signer when history is true", - opts: SignOptions{ - History: true, - Namespace: "test-ns", - }, - setupMocks: func(t *testing.T, clients *resources.MockClientFactory) { - mockResourceClients := resources.NewMockResourceClients(t) - clients.On("Clients", context.Background(), "test-ns").Return(mockResourceClients, nil) - mockResourceClients.On("User", mock.Anything).Return(nil, nil) - }, - expectedType: &loadUsersOnceSigner{}, - }, - { - name: "should return error when clients factory fails", - opts: SignOptions{ - History: true, - Namespace: "test-ns", - }, - setupMocks: func(t *testing.T, clients *resources.MockClientFactory) { - clients.On("Clients", context.Background(), "test-ns").Return(nil, fmt.Errorf("clients error")) - }, - expectedError: "get clients: clients error", - }, - { - name: "should return error when user client fails", - opts: SignOptions{ - History: true, - Namespace: "test-ns", - }, - setupMocks: func(t *testing.T, clients *resources.MockClientFactory) { - mockResourceClients := resources.NewMockResourceClients(t) - clients.On("Clients", context.Background(), "test-ns").Return(mockResourceClients, nil) - mockResourceClients.On("User", mock.Anything).Return(nil, fmt.Errorf("user client error")) - }, - expectedError: "get user client: user client error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClients := resources.NewMockClientFactory(t) - tt.setupMocks(t, mockClients) - - factory := NewSignerFactory(mockClients) - signer, err := factory.New(context.Background(), tt.opts) - - if tt.expectedError != "" { - require.Error(t, err) - require.EqualError(t, err, tt.expectedError) - require.Nil(t, signer) - } else { - require.NoError(t, err) - require.NotNil(t, signer) - require.IsType(t, tt.expectedType, signer, "signer should be of expected type") - } - - mockClients.AssertExpectations(t) - }) - } -} diff --git a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go b/pkg/registry/apis/provisioning/resources/signature/signer_mock.go deleted file mode 100644 index 46cd71dab55..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go +++ /dev/null @@ -1,96 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package signature - -import ( - context "context" - - utils "github.com/grafana/grafana/pkg/apimachinery/utils" - mock "github.com/stretchr/testify/mock" -) - -// MockSigner is an autogenerated mock type for the Signer type -type MockSigner struct { - mock.Mock -} - -type MockSigner_Expecter struct { - mock *mock.Mock -} - -func (_m *MockSigner) EXPECT() *MockSigner_Expecter { - return &MockSigner_Expecter{mock: &_m.Mock} -} - -// Sign provides a mock function with given fields: ctx, item -func (_m *MockSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) { - ret := _m.Called(ctx, item) - - if len(ret) == 0 { - panic("no return value specified for Sign") - } - - var r0 context.Context - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, utils.GrafanaMetaAccessor) (context.Context, error)); ok { - return rf(ctx, item) - } - if rf, ok := ret.Get(0).(func(context.Context, utils.GrafanaMetaAccessor) context.Context); ok { - r0 = rf(ctx, item) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(context.Context) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, utils.GrafanaMetaAccessor) error); ok { - r1 = rf(ctx, item) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockSigner_Sign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sign' -type MockSigner_Sign_Call struct { - *mock.Call -} - -// Sign is a helper method to define mock.On call -// - ctx context.Context -// - item utils.GrafanaMetaAccessor -func (_e *MockSigner_Expecter) Sign(ctx interface{}, item interface{}) *MockSigner_Sign_Call { - return &MockSigner_Sign_Call{Call: _e.mock.On("Sign", ctx, item)} -} - -func (_c *MockSigner_Sign_Call) Run(run func(ctx context.Context, item utils.GrafanaMetaAccessor)) *MockSigner_Sign_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(utils.GrafanaMetaAccessor)) - }) - return _c -} - -func (_c *MockSigner_Sign_Call) Return(_a0 context.Context, _a1 error) *MockSigner_Sign_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockSigner_Sign_Call) RunAndReturn(run func(context.Context, utils.GrafanaMetaAccessor) (context.Context, error)) *MockSigner_Sign_Call { - _c.Call.Return(run) - return _c -} - -// NewMockSigner creates a new instance of MockSigner. 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 NewMockSigner(t interface { - mock.TestingT - Cleanup(func()) -}) *MockSigner { - mock := &MockSigner{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/resources/signature/users.go b/pkg/registry/apis/provisioning/resources/signature/users.go deleted file mode 100644 index 1cf4171ed63..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/users.go +++ /dev/null @@ -1,115 +0,0 @@ -package signature - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - - "github.com/grafana/grafana/apps/provisioning/pkg/repository" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" -) - -const maxUsers = 10000 - -type loadUsersOnceSigner struct { - signatures map[string]repository.CommitSignature - client dynamic.ResourceInterface - once sync.Once - onceErr error -} - -// NewLoadUsersOnceSigner returns a Signer that loads the signatures from users -// it will only load the signatures once and cache them -// if the user is not found, it will use the grafana user as the author -func NewLoadUsersOnceSigner(client dynamic.ResourceInterface) Signer { - return &loadUsersOnceSigner{ - client: client, - once: sync.Once{}, - signatures: map[string]repository.CommitSignature{}, - } -} - -func (s *loadUsersOnceSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) { - if s.onceErr != nil { - return ctx, fmt.Errorf("load signatures: %w", s.onceErr) - } - - var err error - s.once.Do(func() { - s.signatures, err = s.load(ctx, s.client) - s.onceErr = err - }) - if err != nil { - return ctx, fmt.Errorf("load signatures: %w", err) - } - - id := item.GetUpdatedBy() - if id == "" { - id = item.GetCreatedBy() - } - if id == "" { - id = "grafana" - } - - sig := s.signatures[id] // lookup - if sig.Name == "" && sig.Email == "" { - sig.Name = id - } - t, err := item.GetUpdatedTimestamp() - if err == nil && t != nil { - sig.When = *t - } else { - sig.When = item.GetCreationTimestamp().Time - } - - return repository.WithAuthorSignature(ctx, sig), nil -} - -func (s *loadUsersOnceSigner) load(ctx context.Context, client dynamic.ResourceInterface) (map[string]repository.CommitSignature, error) { - userInfo := make(map[string]repository.CommitSignature) - var count int - err := resources.ForEach(ctx, client, func(item *unstructured.Unstructured) error { - count++ - if count > maxUsers { - return errors.New("too many users") - } - - sig := repository.CommitSignature{} - // FIXME: should we improve logging here? - var ( - ok bool - err error - ) - - sig.Name, ok, err = unstructured.NestedString(item.Object, "spec", "login") - if !ok || err != nil { - return nil - } - sig.Email, ok, err = unstructured.NestedString(item.Object, "spec", "email") - if !ok || err != nil { - return nil - } - - if sig.Name == sig.Email { - if sig.Name == "" { - sig.Name = item.GetName() - } else if strings.Contains(sig.Email, "@") { - sig.Email = "" // don't use the same value for name+email - } - } - - userInfo["user:"+item.GetName()] = sig - return nil - }) - if err != nil { - return nil, err - } - - return userInfo, nil -} diff --git a/pkg/registry/apis/provisioning/resources/signature/users_test.go b/pkg/registry/apis/provisioning/resources/signature/users_test.go deleted file mode 100644 index ccd63014a76..00000000000 --- a/pkg/registry/apis/provisioning/resources/signature/users_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package signature - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - - "github.com/grafana/grafana/apps/provisioning/pkg/repository" - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface -type mockDynamicInterface struct { - dynamic.ResourceInterface - items []unstructured.Unstructured - err error -} - -func (m *mockDynamicInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { - if m.err != nil { - return nil, m.err - } - return &unstructured.UnstructuredList{ - Items: m.items, - }, nil -} - -type mockGrafanaMetaAccessor struct { - utils.GrafanaMetaAccessor - createdBy string - updatedBy string - creationTimestamp time.Time - updatedTimestamp *time.Time - updatedTimestampErr error -} - -func (m *mockGrafanaMetaAccessor) GetCreatedBy() string { - return m.createdBy -} - -func (m *mockGrafanaMetaAccessor) GetUpdatedBy() string { - return m.updatedBy -} - -func (m *mockGrafanaMetaAccessor) GetCreationTimestamp() metav1.Time { - return metav1.Time{Time: m.creationTimestamp} -} - -func (m *mockGrafanaMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) { - if m.updatedTimestampErr != nil { - return nil, m.updatedTimestampErr - } - return m.updatedTimestamp, nil -} - -func TestLoadUsersOnceSigner_Sign(t *testing.T) { - baseTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) - updateTime := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC) - - tests := []struct { - name string - items []unstructured.Unstructured - meta *mockGrafanaMetaAccessor - clientErr error - expectedSig repository.CommitSignature - expectedError string - }{ - { - name: "should sign with user info when user exists", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "johndoe", - "email": "john@example.com", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - updatedTimestamp: &updateTime, - }, - expectedSig: repository.CommitSignature{ - Name: "johndoe", - Email: "john@example.com", - When: updateTime, - }, - }, - { - name: "should fallback to created by when updated by is empty", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "johndoe", - "email": "john@example.com", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - createdBy: "user:user1", - creationTimestamp: baseTime, - }, - expectedSig: repository.CommitSignature{ - Name: "johndoe", - Email: "john@example.com", - When: baseTime, - }, - }, - { - name: "should use grafana when no user info available", - meta: &mockGrafanaMetaAccessor{ - creationTimestamp: baseTime, - }, - expectedSig: repository.CommitSignature{ - Name: "grafana", - When: baseTime, - }, - }, - { - name: "should handle user with same login and email", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "john@example.com", - "email": "john@example.com", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - updatedTimestamp: &updateTime, - }, - expectedSig: repository.CommitSignature{ - Name: "john@example.com", - Email: "", - When: updateTime, - }, - }, - { - name: "should handle empty login and email", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "", - "email": "", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - updatedTimestamp: &updateTime, - }, - expectedSig: repository.CommitSignature{ - Name: "user1", - Email: "", - When: updateTime, - }, - }, - { - name: "should handle empty email", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "johndoe", - "email": "", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - updatedTimestamp: &updateTime, - }, - expectedSig: repository.CommitSignature{ - Name: "johndoe", - Email: "", - When: updateTime, - }, - }, - { - name: "should fail when too many users", - items: func() []unstructured.Unstructured { - items := make([]unstructured.Unstructured, maxUsers+1) - for i := 0; i < maxUsers+1; i++ { - items[i] = unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "johndoe", - "email": "john@example.com", - }, - }, - } - } - return items - }(), - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - }, - expectedError: "load signatures: too many users", - }, - { - name: "should handle missing user fields gracefully", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - // missing login and email - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - }, - expectedSig: repository.CommitSignature{ - Name: "user:user1", - When: baseTime, - }, - }, - { - name: "should use creation timestamp when update timestamp has error", - items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": "user1", - }, - "spec": map[string]interface{}{ - "login": "johndoe", - "email": "john@example.com", - }, - }, - }, - }, - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - updatedTimestampErr: errors.New("update timestamp error"), - }, - expectedSig: repository.CommitSignature{ - Name: "johndoe", - Email: "john@example.com", - When: baseTime, - }, - }, - { - name: "should fail when listing users fails", - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - }, - clientErr: fmt.Errorf("failed to list users"), - expectedError: "load signatures: error executing list: failed to list users", - }, - { - name: "should handle empty user list", - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - }, - items: []unstructured.Unstructured{}, - expectedSig: repository.CommitSignature{ - Name: "user:user1", - When: baseTime, - }, - }, - { - name: "should handle multiple calls with error", - meta: &mockGrafanaMetaAccessor{ - updatedBy: "user:user1", - creationTimestamp: baseTime, - }, - clientErr: fmt.Errorf("failed to list users"), - expectedError: "load signatures: error executing list: failed to list users", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := &mockDynamicInterface{ - items: tt.items, - err: tt.clientErr, - } - - signer := NewLoadUsersOnceSigner(client) - ctx := context.Background() - - signedCtx, err := signer.Sign(ctx, tt.meta) - - if tt.expectedError != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tt.expectedError) - - // Test that subsequent calls also fail with the same error - _, err2 := signer.Sign(ctx, tt.meta) - require.Error(t, err2) - require.Contains(t, err2.Error(), tt.expectedError) - return - } - - require.NoError(t, err) - sig := repository.GetAuthorSignature(signedCtx) - require.NotNil(t, sig) - require.Equal(t, tt.expectedSig.Name, sig.Name) - require.Equal(t, tt.expectedSig.Email, sig.Email) - require.Equal(t, tt.expectedSig.When, sig.When) - - // Test that subsequent calls use cached data - signedCtx2, err := signer.Sign(ctx, tt.meta) - require.NoError(t, err) - sig2 := repository.GetAuthorSignature(signedCtx2) - require.Equal(t, sig, sig2) - }) - } -} diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go index 494d47d16e7..c818a76812b 100644 --- a/pkg/registry/apis/provisioning/routes.go +++ b/pkg/registry/apis/provisioning/routes.go @@ -14,7 +14,6 @@ import ( authlib "github.com/grafana/authlib/types" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -156,16 +155,9 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { return } - legacyStorage := false - if b.storageStatus != nil { - legacyStorage = dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus) - } - settings := provisioning.RepositoryViewList{ - Items: make([]provisioning.RepositoryView, len(all)), - AllowedTargets: b.allowedTargets, - // FIXME: this shouldn't be here in provisioning but at the dual writer or something about the storage - LegacyStorage: legacyStorage, + Items: make([]provisioning.RepositoryView, len(all)), + AllowedTargets: b.allowedTargets, AvailableRepositoryTypes: b.repoFactory.Types(), AllowImageRendering: b.allowImageRendering, } diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go index f8e485e8fe5..d6fec09bc96 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/url" - "path" "strings" "time" @@ -141,14 +140,20 @@ func (e *evaluator) evaluateFile(ctx context.Context, repo repository.Reader, ba if info.Parsed.GVK.Kind == dashboardKind { // FIXME: extract the logic out of a dashboard URL builder/injector or similar // for testability and decoupling + urlBuilder, err := url.Parse(baseURL) + if err != nil { + info.Error = err.Error() + return info + } + if info.Parsed.Existing != nil { - info.GrafanaURL = fmt.Sprintf("%sd/%s/%s", baseURL, obj.GetName(), - slugify.Slugify(info.Title)) + grafanaURL := urlBuilder.JoinPath("d", obj.GetName(), slugify.Slugify(info.Title)) + info.GrafanaURL = grafanaURL.String() } // Load this file directly - info.PreviewURL = baseURL + path.Join("admin/provisioning", - info.Parsed.Repo.Name, "dashboard/preview", info.Parsed.Info.Path) + previewURL := urlBuilder.JoinPath("admin/provisioning", info.Parsed.Repo.Name, "dashboard/preview", info.Parsed.Info.Path) + info.PreviewURL = previewURL.String() query := url.Values{} query.Set("ref", info.Parsed.Info.Ref) diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go index c8f0c33e92a..6c513830d29 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go @@ -737,8 +737,78 @@ func TestCalculateChanges(t *testing.T) { Path: "path/to/file.json", Ref: "ref", }, - GrafanaURL: "ht tp://bad url/d/the-uid/hello-world", // Malformed URL - PreviewURL: "ht tp://bad url/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref", + Error: "parse \"ht tp://bad url/\": first path segment in URL cannot contain colon", + }}, + }, + }, + { + name: "path with spaces", + setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) { + finfo := &repository.FileInfo{ + Path: "path/to/file with spaces.json", + Ref: "ref", + Data: []byte("xxxx"), + } + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": resources.DashboardResource.GroupVersion().String(), + "kind": dashboardKind, + "metadata": map[string]interface{}{ + "name": "the-uid", + }, + "spec": map[string]interface{}{ + "title": "hello world", + }, + }, + } + meta, _ := utils.MetaAccessor(obj) + + progress.On("SetMessage", mock.Anything, "process path/to/file with spaces.json").Return() + reader.On("Read", mock.Anything, "path/to/file with spaces.json", "ref").Return(finfo, nil) + reader.On("Config").Return(&provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "x", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + GenerateDashboardPreviews: true, + }, + }, + }) + parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{ + Info: finfo, + Repo: provisioning.ResourceRepositoryInfo{ + Namespace: "x", + Name: "y", + }, + GVK: schema.GroupVersionKind{ + Kind: dashboardKind, + }, + Obj: obj, + Existing: obj, + Meta: meta, + DryRunResponse: obj, + }, nil) + renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false) + parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil) + }, + changes: []repository.VersionedFileChange{{ + Action: repository.FileActionCreated, + Path: "path/to/file with spaces.json", + Ref: "ref", + }}, + expectedInfo: changeInfo{ + Changes: []fileChangeInfo{{ + Change: repository.VersionedFileChange{ + Action: repository.FileActionCreated, + Path: "path/to/file with spaces.json", + Ref: "ref", + }, + GrafanaURL: "http://host/d/the-uid/hello-world", + PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file%20with%20spaces.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref", + GrafanaScreenshotURL: "", + PreviewScreenshotURL: "", }}, }, }, diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 99323eb2721..ad9c9ac9888 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" + "github.com/grafana/authlib/authn" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/api/dtos" @@ -154,11 +156,11 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O } } } - connectLogger.Debug("responder sending status code", "statusCode", statusCode) + connectLogger.Debug("responder sending status code", "statusCode", statusCode, "caller", getCaller(ctx)) }, func(err error) { - connectLogger.Error("error caught in handler", "err", err) + connectLogger.Error("error caught in handler", "err", err, "caller", getCaller(ctx)) span.SetStatus(codes.Error, "query error") if err == nil { @@ -480,3 +482,12 @@ func getValidDataSourceRef(ctx context.Context, ds *v0alpha1.DataSourceRef, id i return ds, nil } + +func getCaller(ctx context.Context) string { + authInfo, ok := claims.AuthInfoFrom(ctx) + if !ok { + return "" + } else { + return strings.Join(authInfo.GetExtra()[authn.ServiceIdentityKey], ",") + } +} diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index b23c78d55bc..56025d3893b 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" dataapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" @@ -180,7 +181,7 @@ func TestQueryAPI(t *testing.T) { legacyDatasourceLookup: &mockLegacyDataSourceLookup{}, } - reqCtx := identity.WithRequester(context.Background(), mockUser{}) + reqCtx := claims.WithAuthInfo(identity.WithRequester(context.Background(), mockUser{}), &mockAuthInfo{}) req := httptest.NewRequestWithContext(reqCtx, http.MethodPost, "/some-path", bytes.NewReader([]byte(tc.queryJSON))) req.Header.Set("Content-Type", "application/json") @@ -439,3 +440,11 @@ func TestMergeHeaders(t *testing.T) { }) } } + +type mockAuthInfo struct { + claims.AuthInfo +} + +func (main mockAuthInfo) GetExtra() map[string][]string { + return nil +} diff --git a/pkg/registry/apis/secret/decrypt/grpc_client.go b/pkg/registry/apis/secret/decrypt/grpc_client.go index f892de5ebe5..0612455c3d8 100644 --- a/pkg/registry/apis/secret/decrypt/grpc_client.go +++ b/pkg/registry/apis/secret/decrypt/grpc_client.go @@ -9,10 +9,13 @@ import ( "maps" "os" "slices" + "time" "github.com/fullstorydev/grpchan" + grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" @@ -72,6 +75,15 @@ func NewGRPCDecryptClientWithTLS( opts = append(opts, grpc.WithDisableServiceConfig()) } + // Add retry interceptor to retry on transient connection issues. + // Retries on ResourceExhausted (per-RPC limits reached) and Unavailable (system unavailable). + retryInterceptor := grpc_retry.UnaryClientInterceptor( + grpc_retry.WithMax(3), + grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)), + grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable), + ) + opts = append(opts, grpc.WithUnaryInterceptor(retryInterceptor)) + conn, err := grpc.NewClient(address, opts...) if err != nil { return nil, fmt.Errorf("failed to connect to grpc decrypt server at %s: %w", address, err) diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 7f51ad844e0..db909b08777 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -12,6 +12,7 @@ import ( service "github.com/grafana/grafana/pkg/apis/service/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -37,7 +38,8 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration bui } func (b *ServiceAPIBuilder) GetAuthorizer() authorizer.Authorizer { - return nil // default authorizer is fine + //nolint:staticcheck // not yet migrated to Resource Authorizer + return roleauthorizer.NewRoleAuthorizer() } func (b *ServiceAPIBuilder) GetGroupVersion() schema.GroupVersion { diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 12153f812a1..d95f01bef36 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -3,6 +3,7 @@ package apiregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/apiserver/auditing" "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -27,12 +28,16 @@ import ( var WireSetExts = wire.NewSet( noopstorage.ProvideStorageBackend, wire.Bind(new(iam.CoreRoleStorageBackend), new(*noopstorage.StorageBackendImpl)), - wire.Bind(new(iam.RoleStorageBackend), new(*noopstorage.StorageBackendImpl)), + iam.ProvideNoopRoleApiInstaller, wire.Bind(new(iam.RoleBindingStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.ExternalGroupMappingStorageBackend), new(*noopstorage.StorageBackendImpl)), externalgroupmapping.ProvideNoopTeamGroupsREST, wire.Bind(new(externalgroupmapping.TeamGroupsHandler), new(*externalgroupmapping.NoopTeamGroupsREST)), + + // Auditing Options + auditing.ProvideNoopBackend, + auditing.ProvideNoopPolicyRuleProvider, ) var provisioningExtras = wire.NewSet( diff --git a/pkg/registry/apps/advisor/accesscontrol.go b/pkg/registry/apps/advisor/accesscontrol.go new file mode 100644 index 00000000000..2b59e55aa4e --- /dev/null +++ b/pkg/registry/apps/advisor/accesscontrol.go @@ -0,0 +1,150 @@ +package advisor + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" +) + +const ( + // Check + ActionAdvisorCheckCreate = "advisor.checks:create" // CREATE. + ActionAdvisorCheckWrite = "advisor.checks:write" // UPDATE. + ActionAdvisorCheckRead = "advisor.checks:read" // GET + LIST. + ActionAdvisorCheckDelete = "advisor.checks:delete" // DELETE. + + // CheckTypes + ActionAdvisorCheckTypesCreate = "advisor.checktypes:create" // CREATE. + ActionAdvisorCheckTypesWrite = "advisor.checktypes:write" // UPDATE. + ActionAdvisorCheckTypesRead = "advisor.checktypes:read" // GET + LIST. + ActionAdvisorCheckTypesDelete = "advisor.checktypes:delete" // DELETE. + + // Register + ActionAdvisorRegisterCreate = "advisor.register:create" // CREATE (register check types). +) + +var ( + ScopeProviderAdvisorCheck = accesscontrol.NewScopeProvider("advisor.checks") + ScopeProviderAdvisorCheckTypes = accesscontrol.NewScopeProvider("advisor.checktypes") + ScopeProviderAdvisorRegister = accesscontrol.NewScopeProvider("advisor.register") + + ScopeAllAdvisorCheck = ScopeProviderAdvisorCheck.GetResourceAllScope() + ScopeAllAdvisorCheckTypes = ScopeProviderAdvisorCheckTypes.GetResourceAllScope() + ScopeAllAdvisorRegister = ScopeProviderAdvisorRegister.GetResourceAllScope() +) + +func registerAccessControlRoles(service accesscontrol.Service) error { + // Check + checkReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:advisor.checks:reader", + DisplayName: "Advisor Check Reader", + Description: "Read and list advisor checks.", + Group: "Advisor", + Permissions: []accesscontrol.Permission{ + { + Action: ActionAdvisorCheckRead, + Scope: ScopeAllAdvisorCheck, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + checkWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:advisor.checks:writer", + DisplayName: "Advisor Check Writer", + Description: "Create, update and delete advisor checks.", + Group: "Advisor", + Permissions: []accesscontrol.Permission{ + { + Action: ActionAdvisorCheckCreate, + Scope: ScopeAllAdvisorCheck, + }, + { + Action: ActionAdvisorCheckRead, + Scope: ScopeAllAdvisorCheck, + }, + { + Action: ActionAdvisorCheckWrite, + Scope: ScopeAllAdvisorCheck, + }, + { + Action: ActionAdvisorCheckDelete, + Scope: ScopeAllAdvisorCheck, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // CheckTypes + checkTypesReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:advisor.checktypes:reader", + DisplayName: "Advisor Check Types Reader", + Description: "Read and list advisor check types.", + Group: "Advisor", + Permissions: []accesscontrol.Permission{ + { + Action: ActionAdvisorCheckTypesRead, + Scope: ScopeAllAdvisorCheckTypes, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + checkTypesWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:advisor.checktypes:writer", + DisplayName: "Advisor Check Types Writer", + Description: "Create, update and delete advisor check types.", + Group: "Advisor", + Permissions: []accesscontrol.Permission{ + { + Action: ActionAdvisorCheckTypesCreate, + Scope: ScopeAllAdvisorCheckTypes, + }, + { + Action: ActionAdvisorCheckTypesRead, + Scope: ScopeAllAdvisorCheckTypes, + }, + { + Action: ActionAdvisorCheckTypesWrite, + Scope: ScopeAllAdvisorCheckTypes, + }, + { + Action: ActionAdvisorCheckTypesDelete, + Scope: ScopeAllAdvisorCheckTypes, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // Register + registerWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:advisor.register:writer", + DisplayName: "Advisor Register Writer", + Description: "Register default advisor check types.", + Group: "Advisor", + Permissions: []accesscontrol.Permission{ + { + Action: ActionAdvisorRegisterCreate, + Scope: ScopeAllAdvisorRegister, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + return service.DeclareFixedRoles( + checkReader, + checkWriter, + checkTypesReader, + checkTypesWriter, + registerWriter, + ) +} diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index 2cafb630803..31ad7d7e7d5 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -1,17 +1,17 @@ package advisor import ( - "github.com/grafana/grafana-app-sdk/app" + "fmt" + + authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" - "github.com/grafana/grafana-app-sdk/simple" - advisorapi "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/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/client-go/rest" ) var ( @@ -20,37 +20,26 @@ var ( ) type AdvisorAppInstaller struct { - appsdkapiserver.AppInstaller -} - -// GetAuthorizer returns the authorizer for the plugins app. -func (a *AdvisorAppInstaller) GetAuthorizer() authorizer.Authorizer { - return advisorapp.GetAuthorizer() + *advisorapp.AdvisorAppInstaller } func ProvideAppInstaller( + accessControlService accesscontrol.Service, + accessClient authlib.AccessClient, checkRegistry checkregistry.CheckService, cfg *setting.Cfg, orgService org.Service, ) (*AdvisorAppInstaller, error) { - provider := simple.NewAppProvider(advisorapi.LocalManifest(), nil, advisorapp.New) - pluginConfig := cfg.PluginSettings["grafana-advisor-app"] - specificConfig := checkregistry.AdvisorAppConfig{ - CheckRegistry: checkRegistry, - PluginConfig: pluginConfig, - StackID: cfg.StackID, - OrgService: orgService, + if err := registerAccessControlRoles(accessControlService); err != nil { + return nil, fmt.Errorf("registering access control roles: %w", err) } - appCfg := app.Config{ - KubeConfig: rest.Config{}, - ManifestData: *advisorapi.LocalManifest().ManifestData, - SpecificConfig: specificConfig, - } - installer := &AdvisorAppInstaller{} - i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appCfg, advisorapi.NewGoTypeAssociator()) + + authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient) + i, err := advisorapp.ProvideAppInstaller(authorizer, checkRegistry, cfg, orgService) if err != nil { return nil, err } - installer.AppInstaller = i - return installer, nil + return &AdvisorAppInstaller{ + AdvisorAppInstaller: i, + }, nil } diff --git a/pkg/registry/apps/alerting/historian/register.go b/pkg/registry/apps/alerting/historian/register.go index 68830dcd0ef..725cb2fae8d 100644 --- a/pkg/registry/apps/alerting/historian/register.go +++ b/pkg/registry/apps/alerting/historian/register.go @@ -1,9 +1,12 @@ package historian import ( + "context" + "github.com/grafana/grafana-app-sdk/app" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/simple" + "k8s.io/apiserver/pkg/authorization/authorizer" restclient "k8s.io/client-go/rest" "github.com/grafana/grafana/apps/alerting/historian/pkg/apis" @@ -23,6 +26,14 @@ type AlertingHistorianAppInstaller struct { appsdkapiserver.AppInstaller } +func (a *AlertingHistorianAppInstaller) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + return authorizer.DecisionAllow, "", nil + }, + ) +} + func RegisterAppInstaller( cfg *setting.Cfg, ng *ngalert.AlertNG, diff --git a/pkg/registry/apps/alerting/notifications/receiver/conversions.go b/pkg/registry/apps/alerting/notifications/receiver/conversions.go index b0d79121912..993c63a46fd 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apps/alerting/notifications/receiver/conversions.go @@ -110,10 +110,11 @@ func convertToK8sResource( } var permissionMapper = map[ngmodels.ReceiverPermission]string{ - ngmodels.ReceiverPermissionReadSecret: "canReadSecrets", - ngmodels.ReceiverPermissionAdmin: "canAdmin", - ngmodels.ReceiverPermissionWrite: "canWrite", - ngmodels.ReceiverPermissionDelete: "canDelete", + ngmodels.ReceiverPermissionReadSecret: "canReadSecrets", + ngmodels.ReceiverPermissionAdmin: "canAdmin", + ngmodels.ReceiverPermissionWrite: "canWrite", + ngmodels.ReceiverPermissionDelete: "canDelete", + ngmodels.ReceiverPermissionModifyProtected: "canModifyProtected", } func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) { diff --git a/pkg/registry/apps/alerting/notifications/register.go b/pkg/registry/apps/alerting/notifications/register.go index fbf75d44bb5..c96fa1759b7 100644 --- a/pkg/registry/apps/alerting/notifications/register.go +++ b/pkg/registry/apps/alerting/notifications/register.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval" "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/services/ngalert" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -97,7 +98,12 @@ func (a AlertingNotificationsAppInstaller) GetLegacyStorage(gvr schema.GroupVers } else if gvr == timeinterval.ResourceInfo.GroupVersionResource() { return timeinterval.NewStorage(api.MuteTimings, namespacer) } else if gvr == templategroup.ResourceInfo.GroupVersionResource() { - return templategroup.NewStorage(api.Templates, namespacer) + srv := api.Templates + //nolint:staticcheck // not yet migrated to OpenFeature + if a.ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) { + srv = srv.WithIncludeImported() + } + return templategroup.NewStorage(srv, namespacer) } else if gvr == routingtree.ResourceInfo.GroupVersionResource() { return routingtree.NewStorage(api.Policies, namespacer) } diff --git a/pkg/registry/apps/alerting/notifications/templategroup/conversions.go b/pkg/registry/apps/alerting/notifications/templategroup/conversions.go index 95cd2399816..f009a0d3329 100644 --- a/pkg/registry/apps/alerting/notifications/templategroup/conversions.go +++ b/pkg/registry/apps/alerting/notifications/templategroup/conversions.go @@ -1,6 +1,7 @@ package templategroup import ( + "github.com/grafana/alerting/definition" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" @@ -36,6 +37,7 @@ func convertToK8sResource(orgID int64, template definitions.NotificationTemplate Spec: model.TemplateGroupSpec{ Title: template.Name, Content: template.Template, + Kind: model.TemplateGroupTemplateKind(template.Kind), }, } result.SetProvenanceStatus(string(template.Provenance)) @@ -50,5 +52,6 @@ func convertToDomainModel(template *model.TemplateGroup) definitions.Notificatio Template: template.Spec.Content, ResourceVersion: template.ResourceVersion, Provenance: definitions.Provenance(ngmodels.ProvenanceNone), + Kind: definition.TemplateKind(template.Spec.Kind), } } diff --git a/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go b/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go index caf2558da96..2b1ff3e6638 100644 --- a/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/grafana/alerting/definition" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -211,6 +212,7 @@ func (s *legacyStorage) defaultTemplate() (definitions.NotificationTemplate, err UID: defaultTemplate.Name, Provenance: definitions.Provenance("system"), Template: defaultTemplate.Template, + Kind: definition.GrafanaTemplateKind, } return dto, nil diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index 9ea31109495..a1ec8aafd65 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -3,6 +3,8 @@ package appregistry import ( "context" + "github.com/grafana/grafana/pkg/registry/apps/quotas" + "github.com/open-feature/go-sdk/openfeature" "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" @@ -44,12 +46,17 @@ func ProvideAppInstallers( exampleAppInstaller *example.ExampleAppInstaller, advisorAppInstaller *advisor.AdvisorAppInstaller, alertingHistorianAppInstaller *historian.AlertingHistorianAppInstaller, + quotasAppInstaller *quotas.QuotasAppInstaller, ) []appsdkapiserver.AppInstaller { + featureClient := openfeature.NewDefaultClient() installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, pluginsApplInstaller, exampleAppInstaller, } + if featureClient.Boolean(context.Background(), featuremgmt.FlagKubernetesUnifiedStorageQuotas, false, openfeature.TransactionContext(context.Background())) { + installers = append(installers, quotasAppInstaller) + } //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) { installers = append(installers, shorturlAppInstaller) diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 6a6f0c9a2aa..737e7c8aa9a 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -3,6 +3,7 @@ package appregistry import ( "testing" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -27,6 +28,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { exampleAppInstaller := &example.ExampleAppInstaller{} advisorAppInstaller := &advisor.AdvisorAppInstaller{} historianAppInstaller := &historian.AlertingHistorianAppInstaller{} + quotasAppInstaller := "as.QuotasAppInstaller{} tests := []struct { name string @@ -43,7 +45,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, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller, quotasAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/correlations/register.go b/pkg/registry/apps/correlations/register.go index 757af68a8ce..2a3b1f0bde5 100644 --- a/pkg/registry/apps/correlations/register.go +++ b/pkg/registry/apps/correlations/register.go @@ -5,6 +5,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" restclient "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" @@ -16,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -60,6 +62,11 @@ func RegisterAppInstaller( return installer, nil } +func (a *AppInstaller) GetAuthorizer() authorizer.Authorizer { + //nolint:staticcheck // not yet migrated to Resource Authorizer + return roleauthorizer.NewRoleAuthorizer() +} + func (a *AppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) rest.Storage { kind := correlationsV0.CorrelationKind() gvr := schema.GroupVersionResource{ diff --git a/pkg/registry/apps/logsdrilldown/authorizer.go b/pkg/registry/apps/logsdrilldown/authorizer.go index f485937121d..b9ba63a3cd2 100644 --- a/pkg/registry/apps/logsdrilldown/authorizer.go +++ b/pkg/registry/apps/logsdrilldown/authorizer.go @@ -24,26 +24,38 @@ func GetAuthorizer() authorizer.Authorizer { return authorizer.DecisionDeny, "valid user is required", err } + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "isGrafanaAdmin", nil + } + // Auth handling for LogsDrilldownDefaults resource if attr.GetResource() == "logsdrilldowndefaults" { // Allow list and get for everyone if attr.GetVerb() == "list" || attr.GetVerb() == "get" { return authorizer.DecisionAllow, "", nil } - // Only allow admins to update (create, update, patch, delete) - if u.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil - } // Deny all other operations for non-admins return authorizer.DecisionDeny, "admin access required", nil } - // check if is admin - if u.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil + p := u.GetPermissions() + + // Auth handling for Logs Drilldown default columns + if attr.GetResource() == "logsdrilldowndefaultcolumns" { + // Allow get for all users + if attr.GetVerb() == "get" { + return authorizer.DecisionAllow, "", nil + } + // require plugins:write permissions for other operations + _, ok := p[accesscontrol.PluginRolePrefix+"write"] + if ok { + return authorizer.DecisionAllow, "user has plugins:write", nil + } else { + return authorizer.DecisionDeny, "user missing plugins:write", nil + } } - p := u.GetPermissions() if len(p) == 0 { return authorizer.DecisionDeny, "no permissions", nil } diff --git a/pkg/registry/apps/playlist/register.go b/pkg/registry/apps/playlist/register.go index 52bbc0210f9..336270098e4 100644 --- a/pkg/registry/apps/playlist/register.go +++ b/pkg/registry/apps/playlist/register.go @@ -6,17 +6,20 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "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/playlist/pkg/apis" playlistv0alpha1 "github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1" playlistapp "github.com/grafana/grafana/apps/playlist/pkg/app" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" playlistsvc "github.com/grafana/grafana/pkg/services/playlist" @@ -63,6 +66,11 @@ func RegisterAppInstaller( return installer, nil } +func (p *PlaylistAppInstaller) GetAuthorizer() authorizer.Authorizer { + //nolint:staticcheck // not yet migrated to Resource Authorizer + return roleauthorizer.NewRoleAuthorizer() +} + // GetLegacyStorage returns the legacy storage for the playlist app. func (p *PlaylistAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage { gvr := playlistv0alpha1.PlaylistKind().GroupVersionResource() diff --git a/pkg/registry/apps/plugins/accesscontrol.go b/pkg/registry/apps/plugins/accesscontrol.go index d41efa86f97..e0a8c9b6c23 100644 --- a/pkg/registry/apps/plugins/accesscontrol.go +++ b/pkg/registry/apps/plugins/accesscontrol.go @@ -13,15 +13,15 @@ const ( ActionPluginsPluginsDelete = "plugins.plugins:delete" // DELETE. // PluginMetas - ActionPluginsPluginsMetaCreate = "plugins.pluginsmeta:create" // CREATE. - ActionPluginsPluginsMetaWrite = "plugins.pluginsmeta:write" // UPDATE. - ActionPluginsPluginsMetaRead = "plugins.pluginsmeta:read" // GET + LIST. - ActionPluginsPluginsMetaDelete = "plugins.pluginsmeta:delete" // DELETE. + ActionPluginsPluginsMetaCreate = "plugins.metas:create" // CREATE. + ActionPluginsPluginsMetaWrite = "plugins.metas:write" // UPDATE. + ActionPluginsPluginsMetaRead = "plugins.metas:read" // GET + LIST. + ActionPluginsPluginsMetaDelete = "plugins.metas:delete" // DELETE. ) var ( ScopeProviderPluginsPlugins = accesscontrol.NewScopeProvider("plugins.plugins") - ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.pluginsmeta") + ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.metas") ScopeAllPluginsPlugins = ScopeProviderPluginsPlugins.GetResourceAllScope() ScopeAllPluginsPluginsMeta = ScopeProviderPluginsPluginsMeta.GetResourceAllScope() @@ -76,7 +76,7 @@ func registerAccessControlRoles(service accesscontrol.Service) error { // PluginMetas pluginsMetaReader := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ - Name: "fixed:plugins.pluginsmeta:reader", + Name: "fixed:plugins.metas:reader", DisplayName: "Plugin Metas Reader", Description: "Read and list plugin metadata.", Group: "Plugins", @@ -92,7 +92,7 @@ func registerAccessControlRoles(service accesscontrol.Service) error { pluginsMetaWriter := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ - Name: "fixed:plugins.pluginsmeta:writer", + Name: "fixed:plugins.metas:writer", DisplayName: "Plugin Metas Writer", Description: "Create, update and delete plugin metadata.", Group: "Plugins", diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index 6831d31ef9b..68082f59713 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -2,16 +2,19 @@ package plugins import ( "fmt" - "os" authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" - "k8s.io/apiserver/pkg/authorization/authorizer" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/configprovider" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) var ( @@ -20,35 +23,36 @@ var ( ) type AppInstaller struct { + metaManager *meta.ProviderManager + cfgProvider configprovider.ConfigProvider + restConfigProvider apiserver.RestConfigProvider + *pluginsapp.PluginAppInstaller } -func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClient authlib.AccessClient) (*AppInstaller, error) { +func ProvideAppInstaller( + cfgProvider configprovider.ConfigProvider, + restConfigProvider apiserver.RestConfigProvider, + pluginStore pluginstore.Store, + pluginAssetsService *pluginassets.Service, + accessControlService accesscontrol.Service, accessClient authlib.AccessClient, +) (*AppInstaller, error) { if err := registerAccessControlRoles(accessControlService); err != nil { return nil, fmt.Errorf("registering access control roles: %w", err) } - grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL") - if grafanaComAPIURL == "" { - grafanaComAPIURL = "https://grafana.com/api/plugins" - } - - coreProvider := meta.NewCoreProvider() - cloudProvider := meta.NewCloudProvider(grafanaComAPIURL) - metaProviderManager := meta.NewProviderManager(coreProvider, cloudProvider) - - i, err := pluginsapp.ProvideAppInstaller(metaProviderManager) + localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService) + metaProviderManager := meta.NewProviderManager(localProvider) + authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient) + i, err := pluginsapp.ProvideAppInstaller(authorizer, metaProviderManager) if err != nil { return nil, err } - i.WithAccessChecker(accessClient) - return &AppInstaller{ + metaManager: metaProviderManager, + cfgProvider: cfgProvider, + restConfigProvider: restConfigProvider, PluginAppInstaller: i, }, nil } - -func (a *AppInstaller) GetAuthorizer() authorizer.Authorizer { - return pluginsapp.GetAuthorizer() -} diff --git a/pkg/registry/apps/quotas/register.go b/pkg/registry/apps/quotas/register.go new file mode 100644 index 00000000000..c6b33bc4d00 --- /dev/null +++ b/pkg/registry/apps/quotas/register.go @@ -0,0 +1,57 @@ +package quotas + +import ( + "github.com/grafana/grafana/apps/quotas/pkg/apis" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "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" + quotasapp "github.com/grafana/grafana/apps/quotas/pkg/app" + roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*QuotasAppInstaller)(nil) +) + +type QuotasAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg +} + +func (a *QuotasAppInstaller) GetAuthorizer() authorizer.Authorizer { + //nolint:staticcheck // not yet migrated to Resource Authorizer + return roleauthorizer.NewRoleAuthorizer() +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + resourceClient resource.ResourceClient, +) (*QuotasAppInstaller, error) { + installer := &QuotasAppInstaller{ + cfg: cfg, + } + specificConfig := "asapp.QuotasAppConfig{ + ResourceClient: resourceClient, + } + provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, quotasapp.New) + + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: specificConfig, + } + 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/wireset.go b/pkg/registry/apps/wireset.go index c57fe84d018..a2cef7b2d55 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/shorturl" ) @@ -29,5 +30,6 @@ var WireSet = wire.NewSet( historian.RegisterAppInstaller, logsdrilldown.RegisterAppInstaller, annotation.RegisterAppInstaller, + quotas.RegisterAppInstaller, example.RegisterAppInstaller, ) diff --git a/pkg/registry/backgroundsvcs/adapter/service_test.go b/pkg/registry/backgroundsvcs/adapter/service_test.go index ed357d66443..275068967c0 100644 --- a/pkg/registry/backgroundsvcs/adapter/service_test.go +++ b/pkg/registry/backgroundsvcs/adapter/service_test.go @@ -55,15 +55,9 @@ func TestServiceAdapter_ErrorHandling(t *testing.T) { adapter := asNamedService(mockSvc) - t.Cleanup(func() { - adapter.StopAsync() - err := adapter.AwaitTerminated(context.Background()) - require.ErrorIs(t, err, expectedErr) - }) - err := adapter.StartAsync(context.Background()) require.NoError(t, err) - err = adapter.AwaitRunning(context.Background()) + err = adapter.AwaitTerminated(context.Background()) require.ErrorIs(t, err, expectedErr) require.True(t, mockSvc.runCalled) }) @@ -95,14 +89,9 @@ func TestServiceAdapter_ErrorHandling(t *testing.T) { adapter := asNamedService(mockSvc) - t.Cleanup(func() { - adapter.StopAsync() - err := adapter.AwaitTerminated(context.Background()) - require.ErrorIs(t, err, expectedErr) - }) err := adapter.StartAsync(context.Background()) require.NoError(t, err) - err = adapter.AwaitRunning(context.Background()) + err = adapter.AwaitTerminated(context.Background()) require.ErrorIs(t, err, expectedErr) require.True(t, mockSvc.runCalled) }) diff --git a/pkg/semconv/Makefile b/pkg/semconv/Makefile index 6544e6ad79e..13058b4d235 100644 --- a/pkg/semconv/Makefile +++ b/pkg/semconv/Makefile @@ -3,7 +3,7 @@ all: go markdown .PHONY: go go: - @docker run --rm -u 1000:1000 -v./model:/tmp/grafana/model -v./:/tmp/grafana/output -v./templates:/tmp/grafana/templates \ + @docker run --rm --platform linux/amd64 -u 1000:1000 -v./model:/tmp/grafana/model -v./:/tmp/grafana/output -v./templates:/tmp/grafana/templates \ otel/semconvgen --yaml-root "/tmp/grafana/model/" \ code --template "/tmp/grafana/templates/template.j2" \ --output "/tmp/grafana/output/attributes.go" @@ -11,7 +11,7 @@ go: .PHONY: markdown markdown: - @docker run --rm -u 1000:1000 -v./model:/tmp/grafana/model -v./:/tmp/grafana/output -v./templates:/tmp/grafana/templates \ + @docker run --rm --platform linux/amd64 -u 1000:1000 -v./model:/tmp/grafana/model -v./:/tmp/grafana/output -v./templates:/tmp/grafana/templates \ otel/semconvgen --yaml-root "/tmp/grafana/model/" \ markdown --markdown-root "/tmp/grafana/output/" @npx --yes -- markdown-toc --bullets "-" --no-first-h1 --no-stripHeadingTags -i README.md || exit 1 diff --git a/pkg/semconv/README.md b/pkg/semconv/README.md index 585034221e0..b80a64ad1c9 100644 --- a/pkg/semconv/README.md +++ b/pkg/semconv/README.md @@ -55,6 +55,7 @@ For more information: | Attribute | Type | Description | Examples | [Requirement Level](https://opentelemetry.io/docs/specs/semconv/general/attribute-requirement-level/) | Stability | |---|---|---|---|---|---| | `grafana.plugin.id` | string | The plugin ID. | `prometheus`; `loki`; `grafana-github-datasource` | `Recommended` | ![Stable](https://img.shields.io/badge/-stable-lightgreen) | +| `grafana.plugin.source.class` | string | The plugin source class. | `core` | `Recommended` | ![Stable](https://img.shields.io/badge/-stable-lightgreen) | | `grafana.plugin.type` | string | The plugin type. | `datasource` | `Recommended` | ![Stable](https://img.shields.io/badge/-stable-lightgreen) | diff --git a/pkg/semconv/attributes.go b/pkg/semconv/attributes.go index c759ab2e9c0..dbc19ead8e5 100644 --- a/pkg/semconv/attributes.go +++ b/pkg/semconv/attributes.go @@ -140,6 +140,27 @@ func GrafanaPluginId(val string) attribute.KeyValue { return grafanaPluginIdKey.String(val) } +// Describes Grafana plugin source attributes. +const ( + // GrafanaPluginSourceClassKey is the attribute Key conforming to the + // "grafana.plugin.source.class" semantic conventions. It represents the + // plugin source class. + // + // Type: Enum + // RequirementLevel: Optional + // Stability: stable + grafanaPluginSourceClassKey = attribute.Key("grafana.plugin.source.class") +) + +var ( + // Core Plugin + GrafanaPluginSourceClassCore = grafanaPluginSourceClassKey.String("core") + // External Plugin + GrafanaPluginSourceClassExternal = grafanaPluginSourceClassKey.String("external") + // Unknown Plugin Source + GrafanaPluginSourceClassUnknown = grafanaPluginSourceClassKey.String("unknown") +) + // Describes Grafana service attributes. const ( // GrafanaServiceNameKey is the attribute Key conforming to the diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index 9998d716e35..30fc157eb75 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -2,9 +2,10 @@ module github.com/grafana/grafana/pkg/semconv go 1.25.5 -require go.opentelemetry.io/otel v1.38.0 +require go.opentelemetry.io/otel v1.39.0 require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index ee3256f1e7a..290db815506 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -1,3 +1,5 @@ +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.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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -6,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/semconv/model/registry/plugin.yml b/pkg/semconv/model/registry/plugin.yml index 431794bcd34..1dc2123b2a3 100644 --- a/pkg/semconv/model/registry/plugin.yml +++ b/pkg/semconv/model/registry/plugin.yml @@ -34,3 +34,26 @@ groups: brief: The plugin type. examples: datasource stability: stable + + - id: registry.grafana.plugin.source + type: attribute_group + display_name: Grafana Plugin Source Attributes + brief: "Describes Grafana plugin source attributes." + attributes: + - id: grafana.plugin.source.class + type: + members: + - id: core + value: "core" + brief: 'Core Plugin' + stability: stable + - id: external + value: "external" + brief: 'External Plugin' + stability: stable + - id: unknown + value: "unknown" + brief: 'Unknown Plugin Source' + stability: stable + brief: The plugin source class. + stability: stable diff --git a/pkg/semconv/model/trace/plugin.yaml b/pkg/semconv/model/trace/plugin.yaml index f25e894efbc..b25e1428454 100644 --- a/pkg/semconv/model/trace/plugin.yaml +++ b/pkg/semconv/model/trace/plugin.yaml @@ -5,4 +5,5 @@ groups: stability: stable attributes: - ref: grafana.plugin.id - - ref: grafana.plugin.type \ No newline at end of file + - ref: grafana.plugin.type + - ref: grafana.plugin.source.class \ No newline at end of file diff --git a/pkg/semconv/plugins.go b/pkg/semconv/plugins.go new file mode 100644 index 00000000000..b9e2b90fd08 --- /dev/null +++ b/pkg/semconv/plugins.go @@ -0,0 +1,19 @@ +package semconv + +import ( + "fmt" + + "go.opentelemetry.io/otel/attribute" +) + +// PluginSourceClass converts a plugin source class to the corresponding semantic convention attribute. +func PluginSourceClass(class fmt.Stringer) attribute.KeyValue { + switch class.String() { + case "core": + return GrafanaPluginSourceClassCore + case "external": + return GrafanaPluginSourceClassExternal + default: + return GrafanaPluginSourceClassUnknown + } +} diff --git a/pkg/server/search_server_distributor_test.go b/pkg/server/search_server_distributor_test.go index 65f6346d0a8..527ced946b1 100644 --- a/pkg/server/search_server_distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -389,7 +389,7 @@ func createBaselineServer(t *testing.T, dbType, dbConnStr string, testNamespaces require.NoError(t, err) tracer := noop.NewTracerProvider().Tracer("test-tracer") require.NoError(t, err) - searchOpts, err := search.NewSearchOptions(features, cfg, tracer, docBuilders, nil, nil) + searchOpts, err := search.NewSearchOptions(features, cfg, docBuilders, nil, nil) require.NoError(t, err) server, err := sql.NewResourceServer(sql.ServerOptions{ DB: nil, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 0d4bb10c0b5..9864bed4e12 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -280,6 +280,7 @@ var wireBasicSet = wire.NewSet( store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, + live.ProvideDashboardActivityChannel, pushhttp.ProvideService, contexthandler.ProvideService, ldapservice.ProvideService, @@ -348,6 +349,7 @@ var wireBasicSet = wire.NewSet( dashboardservice.ProvideDashboardService, dashboardservice.ProvideDashboardProvisioningService, dashboardservice.ProvideDashboardPluginService, + dashboardservice.ProvideDashboardAccessService, dashboardstore.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index d3cf6da6b5a..4ae1194ef28 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apiserver/auditing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/configprovider" "github.com/grafana/grafana/pkg/expr" @@ -89,6 +90,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/shorturl" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" @@ -671,10 +673,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service15, service13, requestConfigProvider) - qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() - exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) - queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) - grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, orgService, eventualRestConfigProvider) + dashboardAccessService := service7.ProvideDashboardAccessService(featureToggles, dashboardServiceImpl) + grafanaLive, err := live.ProvideService(cfg, routeRegisterImpl, plugincontextProvider, pluginstoreService, middlewareHandler, cacheServiceImpl, usageStats, featureToggles, dashboardAccessService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -683,6 +683,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api authnAuthenticator := authnimpl.ProvideAuthnServiceAuthenticateOnly(authnimplService) contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) logger := loggermw.Provide(cfg, featureToggles) + qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() + exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) ngAlert := metrics2.ProvideService() repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service15, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) @@ -707,6 +709,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossSearchUserFilter := filters.ProvideOSSSearchUserFilter() ossService := searchusers.ProvideUsersService(cfg, ossSearchUserFilter, userService) + queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) serviceAccountsProxy, err := proxy.ProvideServiceAccountsProxy(cfg, accessControl, acimplService, featureToggles, serviceAccountPermissionsService, serviceAccountsService, routeRegisterImpl) if err != nil { return nil, err @@ -783,7 +786,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) + appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient) if err != nil { return nil, err } @@ -816,7 +819,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api return nil, err } checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, managedpluginsNoop, noop, ssosettingsimplService, cfg, pluginerrsStore) - advisorAppInstaller, err := advisor2.ProvideAppInstaller(checkregistryService, cfg, orgService) + advisorAppInstaller, err := advisor2.ProvideAppInstaller(acimplService, accessClient, checkregistryService, cfg, orgService) if err != nil { return nil, err } @@ -824,9 +827,15 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) + quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) 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) + backend := auditing.ProvideNoopBackend() + policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider() + 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, backend, policyRuleProvider) if err != nil { return nil, err } @@ -838,7 +847,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService) + zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService, registerer) investigationsAppProvider := investigations.RegisterApp(cfg) appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, cfg) if err != nil { @@ -865,15 +874,17 @@ 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, libraryElementService, publicDashboardServiceImpl, serviceImpl) + dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive) + dashboardsAPIBuilder := dashboard.RegisterAPIService(featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl, dashboardActivityChannel, configProvider) 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() + roleApiInstaller := iam.ProvideNoopRoleApiInstaller() noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, userService, teamService) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -1323,10 +1334,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service15, service13, requestConfigProvider) - qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() - exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) - queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) - grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, orgService, eventualRestConfigProvider) + dashboardAccessService := service7.ProvideDashboardAccessService(featureToggles, dashboardServiceImpl) + grafanaLive, err := live.ProvideService(cfg, routeRegisterImpl, plugincontextProvider, pluginstoreService, middlewareHandler, cacheServiceImpl, usageStats, featureToggles, dashboardAccessService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -1335,6 +1344,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac authnAuthenticator := authnimpl.ProvideAuthnServiceAuthenticateOnly(authnimplService) contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) logger := loggermw.Provide(cfg, featureToggles) + qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() + exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) notificationServiceMock := notifications.MockNotificationService() ngAlert := metrics2.ProvideServiceForTest() repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) @@ -1360,6 +1371,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossSearchUserFilter := filters.ProvideOSSSearchUserFilter() ossService := searchusers.ProvideUsersService(cfg, ossSearchUserFilter, userService) + queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) serviceAccountsProxy, err := proxy.ProvideServiceAccountsProxy(cfg, accessControl, acimplService, featureToggles, serviceAccountPermissionsService, serviceAccountsService, routeRegisterImpl) if err != nil { return nil, err @@ -1436,7 +1448,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) + appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient) if err != nil { return nil, err } @@ -1469,7 +1481,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac return nil, err } checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, managedpluginsNoop, noop, ssosettingsimplService, cfg, pluginerrsStore) - advisorAppInstaller, err := advisor2.ProvideAppInstaller(checkregistryService, cfg, orgService) + advisorAppInstaller, err := advisor2.ProvideAppInstaller(acimplService, accessClient, checkregistryService, cfg, orgService) if err != nil { return nil, err } @@ -1477,9 +1489,15 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) + quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) 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) + backend := auditing.ProvideNoopBackend() + policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider() + 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, backend, policyRuleProvider) if err != nil { return nil, err } @@ -1491,7 +1509,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService) + zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService, registerer) investigationsAppProvider := investigations.RegisterApp(cfg) appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, cfg) if err != nil { @@ -1518,15 +1536,17 @@ 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, libraryElementService, publicDashboardServiceImpl, serviceImpl) + dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive) + dashboardsAPIBuilder := dashboard.RegisterAPIService(featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl, dashboardActivityChannel, configProvider) 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() + roleApiInstaller := iam.ProvideNoopRoleApiInstaller() noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, userService, teamService) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -1782,7 +1802,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, provisioning.ProvideStubProvisioningService, legacy.ProvideMigratorDashboardAccessor, migrations2.ProvideUnifiedMigrator, 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)), migrations3.ProvideDataSourceMigrationService, migrations3.ProvideSecretMigrationProvider, wire.Bind(new(migrations3.SecretMigrationProvider), new(*migrations3.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, migrations2.ProvideUnifiedStorageMigrationService, 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, provisioning.ProvideStubProvisioningService, legacy.ProvideMigratorDashboardAccessor, migrations2.ProvideUnifiedMigrator, 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, live.ProvideDashboardActivityChannel, 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, service7.ProvideDashboardAccessService, 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)), migrations3.ProvideDataSourceMigrationService, migrations3.ProvideSecretMigrationProvider, wire.Bind(new(migrations3.SecretMigrationProvider), new(*migrations3.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, migrations2.ProvideUnifiedStorageMigrationService, 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/acimpl/basic_role_db_seed.go b/pkg/services/accesscontrol/acimpl/basic_role_db_seed.go new file mode 100644 index 00000000000..c6128790d1a --- /dev/null +++ b/pkg/services/accesscontrol/acimpl/basic_role_db_seed.go @@ -0,0 +1,44 @@ +package acimpl + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +const ( + ossBasicRoleSeedLockName = "oss-ac-basic-role-seeder" + ossBasicRoleSeedTimeout = 2 * time.Minute +) + +// refreshBasicRolePermissionsInDB ensures basic role permissions are fully derived from in-memory registrations +func (s *Service) refreshBasicRolePermissionsInDB(ctx context.Context, rolesSnapshot map[string][]accesscontrol.Permission) error { + if s.sql == nil || s.seeder == nil { + return nil + } + + run := func(ctx context.Context) error { + desired := map[accesscontrol.SeedPermission]struct{}{} + for role, permissions := range rolesSnapshot { + for _, permission := range permissions { + desired[accesscontrol.SeedPermission{BuiltInRole: role, Action: permission.Action, Scope: permission.Scope}] = struct{}{} + } + } + s.seeder.SetDesiredPermissions(desired) + return s.seeder.Seed(ctx) + } + + if s.serverLock == nil { + return run(ctx) + } + + var err error + errLock := s.serverLock.LockExecuteAndRelease(ctx, ossBasicRoleSeedLockName, ossBasicRoleSeedTimeout, func(ctx context.Context) { + err = run(ctx) + }) + if errLock != nil { + return errLock + } + return err +} diff --git a/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go b/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go new file mode 100644 index 00000000000..986a32b66fc --- /dev/null +++ b/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go @@ -0,0 +1,128 @@ +package acimpl + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegration_OSSBasicRolePermissions_PersistAndRefreshOnRegisterFixedRoles(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + sql := db.InitTestDB(t) + store := database.ProvideService(sql) + + svc := ProvideOSSService( + setting.NewCfg(), + store, + &resourcepermissions.FakeActionSetSvc{}, + localcache.ProvideService(), + featuremgmt.WithFeatures(), + tracing.InitializeTracerForTest(), + sql, + permreg.ProvidePermissionRegistry(), + nil, + ) + + require.NoError(t, svc.DeclareFixedRoles(accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:role", + Permissions: []accesscontrol.Permission{ + {Action: "test:read", Scope: ""}, + }, + }, + Grants: []string{string(org.RoleViewer)}, + })) + + require.NoError(t, svc.RegisterFixedRoles(ctx)) + + // verify permission is persisted to DB for basic:viewer + require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { + var role accesscontrol.Role + ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) + require.NoError(t, err) + require.True(t, ok) + + var count int64 + count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count() + require.NoError(t, err) + require.Equal(t, int64(1), count) + return nil + })) + + // ensure RegisterFixedRoles refreshes it back to defaults + require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { + ts := time.Now() + var role accesscontrol.Role + ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) + require.NoError(t, err) + require.True(t, ok) + + _, err = sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID) + require.NoError(t, err) + p := accesscontrol.Permission{ + RoleID: role.ID, + Action: "custom:keep", + Scope: "", + Created: ts, + Updated: ts, + } + p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope) + _, err = sess.Table("permission").Insert(&p) + return err + })) + + svc2 := ProvideOSSService( + setting.NewCfg(), + store, + &resourcepermissions.FakeActionSetSvc{}, + localcache.ProvideService(), + featuremgmt.WithFeatures(), + tracing.InitializeTracerForTest(), + sql, + permreg.ProvidePermissionRegistry(), + nil, + ) + require.NoError(t, svc2.DeclareFixedRoles(accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:role", + Permissions: []accesscontrol.Permission{ + {Action: "test:read", Scope: ""}, + }, + }, + Grants: []string{string(org.RoleViewer)}, + })) + require.NoError(t, svc2.RegisterFixedRoles(ctx)) + + require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { + var role accesscontrol.Role + ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) + require.NoError(t, err) + require.True(t, ok) + + var count int64 + count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count() + require.NoError(t, err) + require.Equal(t, int64(1), count) + + count, err = sess.Table("permission").Where("role_id = ? AND action = ?", role.ID, "custom:keep").Count() + require.NoError(t, err) + require.Equal(t, int64(0), count) + return nil + })) +} diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 1ea8bf95f77..3fd419b1f6c 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" + "github.com/grafana/grafana/pkg/services/accesscontrol/seeding" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -96,6 +97,12 @@ func ProvideOSSService( roles: accesscontrol.BuildBasicRoleDefinitions(), store: store, permRegistry: permRegistry, + sql: db, + serverLock: lock, + } + + if backend, ok := store.(*database.AccessControlStore); ok { + s.seeder = seeding.New(log.New("accesscontrol.seeder"), backend, backend) } return s @@ -112,8 +119,11 @@ type Service struct { rolesMu sync.RWMutex roles map[string]*accesscontrol.RoleDTO store accesscontrol.Store + seeder *seeding.Seeder permRegistry permreg.PermissionRegistry isInitialized bool + sql db.DB + serverLock *serverlock.ServerLockService } func (s *Service) GetUsageStats(_ context.Context) map[string]any { @@ -431,17 +441,54 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error { defer span.End() s.rolesMu.Lock() - defer s.rolesMu.Unlock() - + registrations := s.registrations.Slice() s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { s.registerRolesLocked(registration) return true }) s.isInitialized = true + + rolesSnapshot := s.getBasicRolePermissionsLocked() + s.rolesMu.Unlock() + + if s.seeder != nil { + if err := s.seeder.SeedRoles(ctx, registrations); err != nil { + return err + } + if err := s.seeder.RemoveAbsentRoles(ctx); err != nil { + return err + } + } + + if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil { + return err + } + return nil } +// getBasicRolePermissionsSnapshotFromRegistrationsLocked computes the desired basic role permissions from the +// current registration list, using the shared seeding registration logic. +// +// it has to be called while holding the roles lock +func (s *Service) getBasicRolePermissionsLocked() map[string][]accesscontrol.Permission { + desired := map[accesscontrol.SeedPermission]struct{}{} + s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { + seeding.AppendDesiredPermissions(desired, s.log, ®istration.Role, registration.Grants, registration.Exclude, true) + return true + }) + + out := make(map[string][]accesscontrol.Permission) + for sp := range desired { + out[sp.BuiltInRole] = append(out[sp.BuiltInRole], accesscontrol.Permission{ + Action: sp.Action, + Scope: sp.Scope, + }) + } + return out +} + // registerRolesLocked processes a single role registration and adds permissions to basic roles. // Must be called with s.rolesMu locked. func (s *Service) registerRolesLocked(registration accesscontrol.RoleRegistration) { @@ -474,6 +521,7 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs defer span.End() acRegs := pluginutils.ToRegistrations(ID, name, regs) + updatedBasicRoles := false for _, r := range acRegs { if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil { return err @@ -500,11 +548,23 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs if initialized { s.rolesMu.Lock() s.registerRolesLocked(r) + updatedBasicRoles = true s.rolesMu.Unlock() s.cache.Flush() } } + if updatedBasicRoles { + s.rolesMu.RLock() + rolesSnapshot := s.getBasicRolePermissionsLocked() + s.rolesMu.RUnlock() + + // plugin roles can be declared after startup - keep DB in sync + if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil { + return err + } + } + return nil } diff --git a/pkg/services/accesscontrol/database/seeder.go b/pkg/services/accesscontrol/database/seeder.go new file mode 100644 index 00000000000..2f53d20b514 --- /dev/null +++ b/pkg/services/accesscontrol/database/seeder.go @@ -0,0 +1,623 @@ +package database + +import ( + "context" + "strings" + "time" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/seeding" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util/xorm/core" +) + +const basicRolePermBatchSize = 500 + +// LoadRoles returns all fixed and plugin roles (global org) with permissions, indexed by role name. +func (s *AccessControlStore) LoadRoles(ctx context.Context) (map[string]*accesscontrol.RoleDTO, error) { + out := map[string]*accesscontrol.RoleDTO{} + + err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { + type roleRow struct { + ID int64 `xorm:"id"` + OrgID int64 `xorm:"org_id"` + Version int64 `xorm:"version"` + UID string `xorm:"uid"` + Name string `xorm:"name"` + DisplayName string `xorm:"display_name"` + Description string `xorm:"description"` + Group string `xorm:"group_name"` + Hidden bool `xorm:"hidden"` + Updated time.Time `xorm:"updated"` + Created time.Time `xorm:"created"` + } + + roles := []roleRow{} + if err := sess.Table("role"). + Where("org_id = ?", accesscontrol.GlobalOrgID). + Where("(name LIKE ? OR name LIKE ?)", accesscontrol.FixedRolePrefix+"%", accesscontrol.PluginRolePrefix+"%"). + Find(&roles); err != nil { + return err + } + + if len(roles) == 0 { + return nil + } + + roleIDs := make([]any, 0, len(roles)) + roleByID := make(map[int64]*accesscontrol.RoleDTO, len(roles)) + for _, r := range roles { + dto := &accesscontrol.RoleDTO{ + ID: r.ID, + OrgID: r.OrgID, + Version: r.Version, + UID: r.UID, + Name: r.Name, + DisplayName: r.DisplayName, + Description: r.Description, + Group: r.Group, + Hidden: r.Hidden, + Updated: r.Updated, + Created: r.Created, + } + out[dto.Name] = dto + roleByID[dto.ID] = dto + roleIDs = append(roleIDs, dto.ID) + } + + type permRow struct { + RoleID int64 `xorm:"role_id"` + Action string `xorm:"action"` + Scope string `xorm:"scope"` + } + perms := []permRow{} + if err := sess.Table("permission").In("role_id", roleIDs...).Find(&perms); err != nil { + return err + } + + for _, p := range perms { + dto := roleByID[p.RoleID] + if dto == nil { + continue + } + dto.Permissions = append(dto.Permissions, accesscontrol.Permission{ + RoleID: p.RoleID, + Action: p.Action, + Scope: p.Scope, + }) + } + + return nil + }) + + return out, err +} + +func (s *AccessControlStore) SetRole(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error { + if existingRole == nil { + return nil + } + + return s.sql.WithDbSession(ctx, func(sess *db.Session) error { + _, err := sess.Table("role"). + Where("id = ? AND org_id = ?", existingRole.ID, accesscontrol.GlobalOrgID). + Update(map[string]any{ + "display_name": wantedRole.DisplayName, + "description": wantedRole.Description, + "group_name": wantedRole.Group, + "hidden": wantedRole.Hidden, + "updated": time.Now(), + }) + return err + }) +} + +func (s *AccessControlStore) SetPermissions(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error { + if existingRole == nil { + return nil + } + + type key struct{ Action, Scope string } + existing := map[key]struct{}{} + for _, p := range existingRole.Permissions { + existing[key{p.Action, p.Scope}] = struct{}{} + } + desired := map[key]struct{}{} + for _, p := range wantedRole.Permissions { + desired[key{p.Action, p.Scope}] = struct{}{} + } + + toAdd := make([]accesscontrol.Permission, 0) + toRemove := make([]accesscontrol.SeedPermission, 0) + + now := time.Now() + for k := range desired { + if _, ok := existing[k]; ok { + continue + } + perm := accesscontrol.Permission{ + RoleID: existingRole.ID, + Action: k.Action, + Scope: k.Scope, + Created: now, + Updated: now, + } + perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) + toAdd = append(toAdd, perm) + } + + for k := range existing { + if _, ok := desired[k]; ok { + continue + } + toRemove = append(toRemove, accesscontrol.SeedPermission{Action: k.Action, Scope: k.Scope}) + } + + if len(toAdd) == 0 && len(toRemove) == 0 { + return nil + } + + return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + if len(toRemove) > 0 { + if err := DeleteRolePermissionTuples(sess, s.sql.GetDBType(), existingRole.ID, toRemove); err != nil { + return err + } + } + + if len(toAdd) > 0 { + _, err := sess.InsertMulti(toAdd) + return err + } + + return nil + }) +} + +func (s *AccessControlStore) CreateRole(ctx context.Context, role accesscontrol.RoleDTO) error { + now := time.Now() + uid := role.UID + if uid == "" && (strings.HasPrefix(role.Name, accesscontrol.FixedRolePrefix) || strings.HasPrefix(role.Name, accesscontrol.PluginRolePrefix)) { + uid = accesscontrol.PrefixedRoleUID(role.Name) + } + r := accesscontrol.Role{ + OrgID: accesscontrol.GlobalOrgID, + Version: role.Version, + UID: uid, + Name: role.Name, + DisplayName: role.DisplayName, + Description: role.Description, + Group: role.Group, + Hidden: role.Hidden, + Created: now, + Updated: now, + } + if r.Version == 0 { + r.Version = 1 + } + + return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + if _, err := sess.Insert(&r); err != nil { + return err + } + + if len(role.Permissions) == 0 { + return nil + } + + // De-duplicate permissions on (action, scope) to avoid unique constraint violations. + // Some role definitions may accidentally include duplicates. + type permKey struct{ Action, Scope string } + seen := make(map[permKey]struct{}, len(role.Permissions)) + + perms := make([]accesscontrol.Permission, 0, len(role.Permissions)) + for _, p := range role.Permissions { + k := permKey{Action: p.Action, Scope: p.Scope} + if _, ok := seen[k]; ok { + continue + } + seen[k] = struct{}{} + + perm := accesscontrol.Permission{ + RoleID: r.ID, + Action: p.Action, + Scope: p.Scope, + Created: now, + Updated: now, + } + perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) + perms = append(perms, perm) + } + _, err := sess.InsertMulti(perms) + return err + }) +} + +func (s *AccessControlStore) DeleteRoles(ctx context.Context, roleUIDs []string) error { + if len(roleUIDs) == 0 { + return nil + } + + uids := make([]any, 0, len(roleUIDs)) + for _, uid := range roleUIDs { + uids = append(uids, uid) + } + + return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + type row struct { + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + } + rows := []row{} + if err := sess.Table("role"). + Where("org_id = ?", accesscontrol.GlobalOrgID). + In("uid", uids...). + Find(&rows); err != nil { + return err + } + if len(rows) == 0 { + return nil + } + + roleIDs := make([]any, 0, len(rows)) + for _, r := range rows { + roleIDs = append(roleIDs, r.ID) + } + + // Remove permissions and assignments first to avoid FK issues (if enabled). + { + args := append([]any{"DELETE FROM permission WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) + if _, err := sess.Exec(args...); err != nil { + return err + } + } + { + args := append([]any{"DELETE FROM user_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) + if _, err := sess.Exec(args...); err != nil { + return err + } + } + { + args := append([]any{"DELETE FROM team_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) + if _, err := sess.Exec(args...); err != nil { + return err + } + } + { + args := append([]any{"DELETE FROM builtin_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) + if _, err := sess.Exec(args...); err != nil { + return err + } + } + + args := append([]any{"DELETE FROM role WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")", accesscontrol.GlobalOrgID}, uids...) + _, err := sess.Exec(args...) + return err + }) +} + +// OSS basic-role permission refresh uses seeding.Seeder.Seed() with a desired set computed in memory. +// These methods implement the permission seeding part of seeding.SeedingBackend against the current permission table. +func (s *AccessControlStore) LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { + var out map[accesscontrol.SeedPermission]struct{} + err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { + rows, err := LoadBasicRoleSeedPermissions(sess) + if err != nil { + return err + } + + out = make(map[accesscontrol.SeedPermission]struct{}, len(rows)) + for _, r := range rows { + r.Origin = "" + out[r] = struct{}{} + } + return nil + }) + return out, err +} + +func (s *AccessControlStore) Apply(ctx context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error { + rolesToUpgrade := seeding.RolesToUpgrade(added, removed) + + // Run the same OSS apply logic as ossBasicRoleSeedBackend.Apply inside a single transaction. + return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + defs := accesscontrol.BuildBasicRoleDefinitions() + builtinToRoleID, err := EnsureBasicRolesExist(sess, defs) + if err != nil { + return err + } + + backend := &ossBasicRoleSeedBackend{ + sess: sess, + now: time.Now(), + builtinToRoleID: builtinToRoleID, + desired: nil, + dbType: s.sql.GetDBType(), + } + if err := backend.Apply(ctx, added, removed, updated); err != nil { + return err + } + + return BumpBasicRoleVersions(sess, rolesToUpgrade) + }) +} + +// EnsureBasicRolesExist ensures the built-in basic roles exist in the role table and are bound in builtin_role. +// It returns a mapping from builtin role name (for example "Admin") to role ID. +func EnsureBasicRolesExist(sess *db.Session, defs map[string]*accesscontrol.RoleDTO) (map[string]int64, error) { + uidToBuiltin := make(map[string]string, len(defs)) + uids := make([]any, 0, len(defs)) + for builtin, def := range defs { + uidToBuiltin[def.UID] = builtin + uids = append(uids, def.UID) + } + + type roleRow struct { + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + } + + rows := []roleRow{} + if err := sess.Table("role"). + Where("org_id = ?", accesscontrol.GlobalOrgID). + In("uid", uids...). + Find(&rows); err != nil { + return nil, err + } + + ts := time.Now() + + builtinToRoleID := make(map[string]int64, len(defs)) + for _, r := range rows { + br, ok := uidToBuiltin[r.UID] + if !ok { + continue + } + builtinToRoleID[br] = r.ID + } + + for builtin, def := range defs { + roleID, ok := builtinToRoleID[builtin] + if !ok { + role := accesscontrol.Role{ + OrgID: def.OrgID, + Version: def.Version, + UID: def.UID, + Name: def.Name, + DisplayName: def.DisplayName, + Description: def.Description, + Group: def.Group, + Hidden: def.Hidden, + Created: ts, + Updated: ts, + } + if _, err := sess.Insert(&role); err != nil { + return nil, err + } + roleID = role.ID + builtinToRoleID[builtin] = roleID + } + + has, err := sess.Table("builtin_role"). + Where("role_id = ? AND role = ? AND org_id = ?", roleID, builtin, accesscontrol.GlobalOrgID). + Exist() + if err != nil { + return nil, err + } + if !has { + br := accesscontrol.BuiltinRole{ + RoleID: roleID, + OrgID: accesscontrol.GlobalOrgID, + Role: builtin, + Created: ts, + Updated: ts, + } + if _, err := sess.Table("builtin_role").Insert(&br); err != nil { + return nil, err + } + } + } + + return builtinToRoleID, nil +} + +// DeleteRolePermissionTuples deletes permissions for a single role by (action, scope) pairs. +// +// It uses a row-constructor IN clause where supported (MySQL, Postgres, SQLite) and falls back +// to a WHERE ... OR ... form for MSSQL. +func DeleteRolePermissionTuples(sess *db.Session, dbType core.DbType, roleID int64, perms []accesscontrol.SeedPermission) error { + if len(perms) == 0 { + return nil + } + + if dbType == migrator.MSSQL { + // MSSQL doesn't support (action, scope) IN ((?,?),(?,?)) row constructors. + where := make([]string, 0, len(perms)) + args := make([]any, 0, 1+len(perms)*2) + args = append(args, roleID) + for _, p := range perms { + where = append(where, "(action = ? AND scope = ?)") + args = append(args, p.Action, p.Scope) + } + _, err := sess.Exec( + append([]any{ + "DELETE FROM permission WHERE role_id = ? AND (" + strings.Join(where, " OR ") + ")", + }, args...)..., + ) + return err + } + + args := make([]any, 0, 1+len(perms)*2) + args = append(args, roleID) + for _, p := range perms { + args = append(args, p.Action, p.Scope) + } + sql := "DELETE FROM permission WHERE role_id = ? AND (action, scope) IN (" + + strings.Repeat("(?, ?),", len(perms)-1) + "(?, ?))" + _, err := sess.Exec(append([]any{sql}, args...)...) + return err +} + +type ossBasicRoleSeedBackend struct { + sess *db.Session + now time.Time + builtinToRoleID map[string]int64 + desired map[accesscontrol.SeedPermission]struct{} + dbType core.DbType +} + +func (b *ossBasicRoleSeedBackend) LoadPrevious(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { + rows, err := LoadBasicRoleSeedPermissions(b.sess) + if err != nil { + return nil, err + } + + out := make(map[accesscontrol.SeedPermission]struct{}, len(rows)) + for _, r := range rows { + // Ensure the key matches what OSS seeding uses (Origin is always empty for basic role refresh). + r.Origin = "" + out[r] = struct{}{} + } + return out, nil +} + +func (b *ossBasicRoleSeedBackend) LoadDesired(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { + return b.desired, nil +} + +func (b *ossBasicRoleSeedBackend) Apply(_ context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error { + // Delete removed permissions (this includes user-defined permissions that aren't in desired). + if len(removed) > 0 { + permsByRoleID := map[int64][]accesscontrol.SeedPermission{} + for _, p := range removed { + roleID, ok := b.builtinToRoleID[p.BuiltInRole] + if !ok { + continue + } + permsByRoleID[roleID] = append(permsByRoleID[roleID], p) + } + + for roleID, perms := range permsByRoleID { + // Chunk to keep statement sizes and parameter counts bounded. + if err := batch(len(perms), basicRolePermBatchSize, func(start, end int) error { + return DeleteRolePermissionTuples(b.sess, b.dbType, roleID, perms[start:end]) + }); err != nil { + return err + } + } + } + + // Insert added permissions and updated-target permissions. + toInsertSeed := make([]accesscontrol.SeedPermission, 0, len(added)+len(updated)) + toInsertSeed = append(toInsertSeed, added...) + for _, v := range updated { + toInsertSeed = append(toInsertSeed, v) + } + if len(toInsertSeed) == 0 { + return nil + } + + // De-duplicate on (role_id, action, scope). This avoids unique constraint violations when: + // - the same permission appears in both added and updated + // - multiple plugin origins grant the same permission (Origin is not persisted in permission table) + type permKey struct { + RoleID int64 + Action string + Scope string + } + seen := make(map[permKey]struct{}, len(toInsertSeed)) + + toInsert := make([]accesscontrol.Permission, 0, len(toInsertSeed)) + for _, p := range toInsertSeed { + roleID, ok := b.builtinToRoleID[p.BuiltInRole] + if !ok { + continue + } + k := permKey{RoleID: roleID, Action: p.Action, Scope: p.Scope} + if _, ok := seen[k]; ok { + continue + } + seen[k] = struct{}{} + + perm := accesscontrol.Permission{ + RoleID: roleID, + Action: p.Action, + Scope: p.Scope, + Created: b.now, + Updated: b.now, + } + perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) + toInsert = append(toInsert, perm) + } + + return batch(len(toInsert), basicRolePermBatchSize, func(start, end int) error { + // MySQL: ignore conflicts to make seeding idempotent under retries/concurrency. + // Conflicts can happen if the same permission already exists (unique on role_id, action, scope). + if b.dbType == migrator.MySQL { + args := make([]any, 0, (end-start)*8) + for i := start; i < end; i++ { + p := toInsert[i] + args = append(args, p.RoleID, p.Action, p.Scope, p.Kind, p.Attribute, p.Identifier, p.Updated, p.Created) + } + sql := append([]any{`INSERT IGNORE INTO permission (role_id, action, scope, kind, attribute, identifier, updated, created) VALUES ` + + strings.Repeat("(?, ?, ?, ?, ?, ?, ?, ?),", end-start-1) + "(?, ?, ?, ?, ?, ?, ?, ?)"}, args...) + _, err := b.sess.Exec(sql...) + return err + } + + _, err := b.sess.InsertMulti(toInsert[start:end]) + return err + }) +} + +func batch(count, size int, eachFn func(start, end int) error) error { + for i := 0; i < count; { + end := i + size + if end > count { + end = count + } + if err := eachFn(i, end); err != nil { + return err + } + i = end + } + return nil +} + +// BumpBasicRoleVersions increments the role version for the given builtin basic roles (Viewer/Editor/Admin/Grafana Admin). +// Unknown role names are ignored. +func BumpBasicRoleVersions(sess *db.Session, basicRoles []string) error { + if len(basicRoles) == 0 { + return nil + } + + defs := accesscontrol.BuildBasicRoleDefinitions() + uids := make([]any, 0, len(basicRoles)) + for _, br := range basicRoles { + def, ok := defs[br] + if !ok { + continue + } + uids = append(uids, def.UID) + } + if len(uids) == 0 { + return nil + } + + sql := "UPDATE role SET version = version + 1 WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")" + _, err := sess.Exec(append([]any{sql, accesscontrol.GlobalOrgID}, uids...)...) + return err +} + +// LoadBasicRoleSeedPermissions returns the current (builtin_role, action, scope) permissions granted to basic roles. +// It sets Origin to empty. +func LoadBasicRoleSeedPermissions(sess *db.Session) ([]accesscontrol.SeedPermission, error) { + rows := []accesscontrol.SeedPermission{} + err := sess.SQL( + `SELECT role.display_name AS builtin_role, p.action, p.scope, '' AS origin + FROM role INNER JOIN permission AS p ON p.role_id = role.id + WHERE role.org_id = ? AND role.name LIKE 'basic:%'`, + accesscontrol.GlobalOrgID, + ).Find(&rows) + return rows, err +} diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index a16f6534082..ab27972e86e 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -6,6 +6,8 @@ import ( "strconv" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/otel" claims "github.com/grafana/authlib/types" @@ -13,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -33,12 +36,15 @@ type ZanzanaReconciler struct { store db.DB client zanzana.Client lock *serverlock.ServerLockService + metrics struct { + lastSuccess prometheus.Gauge + } // reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store. // These are run periodically to try to maintain a consistent state. reconcilers []resourceReconciler } -func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureToggles, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler { +func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureToggles, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service, reg prometheus.Registerer) *ZanzanaReconciler { zanzanaReconciler := &ZanzanaReconciler{ cfg: cfg, log: reconcilerLogger, @@ -92,6 +98,13 @@ func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureTogg }, } + if reg != nil { + zanzanaReconciler.metrics.lastSuccess = promauto.With(reg).NewGauge(prometheus.GaugeOpts{ + Name: "grafana_zanzana_reconcile_last_success_timestamp_seconds", + Help: "Unix timestamp (seconds) when the Zanzana reconciler last completed a reconciliation cycle.", + }) + } + if cfg.Anonymous.Enabled { zanzanaReconciler.reconcilers = append(zanzanaReconciler.reconcilers, newResourceReconciler( @@ -118,6 +131,9 @@ func (r *ZanzanaReconciler) Run(ctx context.Context) error { // Reconcile schedules as job that will run and reconcile resources between // legacy access control and zanzana. func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { + // Ensure we don't reconcile an empty/partial RBAC state before OSS has seeded basic role permissions. + // This matters most during startup where fixed-role loading + basic-role permission refresh runs as another background service. + r.waitForBasicRolesSeeded(ctx) r.reconcile(ctx) // FIXME: @@ -133,6 +149,57 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { } } +func (r *ZanzanaReconciler) hasBasicRolePermissions(ctx context.Context) bool { + var count int64 + // Basic role permissions are stored on "basic:%" roles in the global org (0). + // In a fresh DB, this will be empty until fixed roles are registered and the basic role permission refresh runs. + type row struct { + Count int64 `xorm:"count"` + } + _ = r.store.WithDbSession(ctx, func(sess *db.Session) error { + var rr row + _, err := sess.SQL( + `SELECT COUNT(*) AS count + FROM role INNER JOIN permission AS p ON p.role_id = role.id + WHERE role.org_id = ? AND role.name LIKE ?`, + accesscontrol.GlobalOrgID, + accesscontrol.BasicRolePrefix+"%", + ).Get(&rr) + if err != nil { + return err + } + count = rr.Count + return nil + }) + return count > 0 +} + +func (r *ZanzanaReconciler) waitForBasicRolesSeeded(ctx context.Context) { + // Best-effort: don't block forever. If we can't observe basic roles, proceed anyway. + const ( + maxWait = 15 * time.Second + interval = 1 * time.Second + ) + + deadline := time.NewTimer(maxWait) + defer deadline.Stop() + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + if r.hasBasicRolePermissions(ctx) { + return + } + select { + case <-ctx.Done(): + return + case <-deadline.C: + return + case <-ticker.C: + } + } +} + func (r *ZanzanaReconciler) reconcile(ctx context.Context) { run := func(ctx context.Context, namespace string) { now := time.Now() @@ -144,6 +211,9 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) { r.log.Warn("Failed to perform reconciliation for resource", "err", err) } } + if r.metrics.lastSuccess != nil { + r.metrics.lastSuccess.SetToCurrentTime() + } r.log.Debug("Finished reconciliation", "elapsed", time.Since(now)) } diff --git a/pkg/services/accesscontrol/dualwrite/reconciler_test.go b/pkg/services/accesscontrol/dualwrite/reconciler_test.go new file mode 100644 index 00000000000..0defea011a0 --- /dev/null +++ b/pkg/services/accesscontrol/dualwrite/reconciler_test.go @@ -0,0 +1,67 @@ +package dualwrite + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func TestZanzanaReconciler_hasBasicRolePermissions(t *testing.T) { + env := setupTestEnv(t) + + r := &ZanzanaReconciler{ + store: env.db, + } + + ctx := context.Background() + require.False(t, r.hasBasicRolePermissions(ctx)) + + err := env.db.WithDbSession(ctx, func(sess *db.Session) error { + now := time.Now() + + _, err := sess.Exec( + `INSERT INTO role (org_id, uid, name, display_name, group_name, description, hidden, version, created, updated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + accesscontrol.GlobalOrgID, + "basic_viewer_uid_test", + accesscontrol.BasicRolePrefix+"viewer", + "Viewer", + "Basic", + "Viewer role", + false, + 1, + now, + now, + ) + if err != nil { + return err + } + + var roleID int64 + if _, err := sess.SQL(`SELECT id FROM role WHERE org_id = ? AND uid = ?`, accesscontrol.GlobalOrgID, "basic_viewer_uid_test").Get(&roleID); err != nil { + return err + } + + _, err = sess.Exec( + `INSERT INTO permission (role_id, action, scope, kind, attribute, identifier, created, updated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + roleID, + "dashboards:read", + "dashboards:*", + "", + "", + "", + now, + now, + ) + return err + }) + require.NoError(t, err) + + require.True(t, r.hasBasicRolePermissions(ctx)) +} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index dfda78ac9f0..85df44750d2 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -1,6 +1,7 @@ package accesscontrol import ( + "context" "encoding/json" "errors" "fmt" @@ -447,6 +448,7 @@ const ( ActionAlertingNotificationsTemplatesRead = "alert.notifications.templates:read" ActionAlertingNotificationsTemplatesWrite = "alert.notifications.templates:write" ActionAlertingNotificationsTemplatesDelete = "alert.notifications.templates:delete" + ActionAlertingNotificationsTemplatesTest = "alert.notifications.templates.test:write" // Alerting notifications time interval actions ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" @@ -459,6 +461,7 @@ const ( ActionAlertingReceiversReadSecrets = "alert.notifications.receivers.secrets:read" ActionAlertingReceiversCreate = "alert.notifications.receivers:create" ActionAlertingReceiversUpdate = "alert.notifications.receivers:write" + ActionAlertingReceiversUpdateProtected = "alert.notifications.receivers.protected:write" ActionAlertingReceiversDelete = "alert.notifications.receivers:delete" ActionAlertingReceiversTest = "alert.notifications.receivers:test" ActionAlertingReceiversPermissionsRead = "receivers.permissions:read" @@ -592,3 +595,18 @@ type QueryWithOrg struct { OrgId *int64 `json:"orgId"` Global bool `json:"global"` } + +type SeedPermission struct { + BuiltInRole string `xorm:"builtin_role"` + Action string `xorm:"action"` + Scope string `xorm:"scope"` + Origin string `xorm:"origin"` +} + +type RoleStore interface { + LoadRoles(ctx context.Context) (map[string]*RoleDTO, error) + SetRole(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error + SetPermissions(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error + CreateRole(ctx context.Context, role RoleDTO) error + DeleteRoles(ctx context.Context, roleUIDs []string) error +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go index 49ca4a0b3e8..9dd4def1db3 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go @@ -23,7 +23,7 @@ import ( var ReceiversViewActions = []string{accesscontrol.ActionAlertingReceiversRead} var ReceiversEditActions = append(ReceiversViewActions, []string{accesscontrol.ActionAlertingReceiversUpdate, accesscontrol.ActionAlertingReceiversDelete}...) -var ReceiversAdminActions = append(ReceiversEditActions, []string{accesscontrol.ActionAlertingReceiversReadSecrets, accesscontrol.ActionAlertingReceiversPermissionsRead, accesscontrol.ActionAlertingReceiversPermissionsWrite}...) +var ReceiversAdminActions = append(ReceiversEditActions, []string{accesscontrol.ActionAlertingReceiversReadSecrets, accesscontrol.ActionAlertingReceiversPermissionsRead, accesscontrol.ActionAlertingReceiversPermissionsWrite, accesscontrol.ActionAlertingReceiversUpdateProtected}...) // defaultPermissions returns the default permissions for a newly created receiver. func defaultPermissions() []accesscontrol.SetResourcePermissionCommand { diff --git a/pkg/services/accesscontrol/permreg/permreg.go b/pkg/services/accesscontrol/permreg/permreg.go index c9f010c2909..cae7b701cf0 100644 --- a/pkg/services/accesscontrol/permreg/permreg.go +++ b/pkg/services/accesscontrol/permreg/permreg.go @@ -85,7 +85,10 @@ func newPermissionRegistry() *permissionRegistry { "orgs": "orgs:id:", "plugins": "plugins:id:", "plugins.plugins": "plugins.plugins:uid:", - "plugins.pluginsmeta": "plugins.pluginsmeta:uid:", + "plugins.metas": "plugins.metas:uid:", + "advisor.checks": "advisor.checks:uid:", + "advisor.checktypes": "advisor.checktypes:uid:", + "advisor.register": "advisor.register:uid:", "provisioners": "provisioners:", "reports": "reports:id:", "permissions": "permissions:type:", diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 981a99f5189..3c6d1da2038 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -33,6 +33,7 @@ type api struct { permissions []string features featuremgmt.FeatureToggles restConfigProvider apiserver.RestConfigProvider + logger log.Logger } func newApi(cfg *setting.Cfg, ac accesscontrol.AccessControl, router routing.RouteRegister, manager *Service, features featuremgmt.FeatureToggles, restConfigProvider apiserver.RestConfigProvider) *api { @@ -41,7 +42,23 @@ func newApi(cfg *setting.Cfg, ac accesscontrol.AccessControl, router routing.Rou for i := len(manager.permissions) - 1; i >= 0; i-- { permissions = append(permissions, manager.permissions[i]) } - return &api{cfg, ac, router, manager, permissions, features, restConfigProvider} + return &api{ + cfg: cfg, + ac: ac, + router: router, + service: manager, + permissions: permissions, + features: features, + restConfigProvider: restConfigProvider, + logger: log.New("resource-permissions-api"), + } +} + +// shouldUseK8sAPIs returns true if both feature flags for K8s API redirect are enabled +func (a *api) shouldUseK8sAPIs() bool { + //nolint:staticcheck // not yet migrated to OpenFeature + return a.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthZHandlerRedirect) && + a.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) } func (a *api) registerEndpoints() { @@ -189,11 +206,10 @@ func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, k8sPermissions) } span.RecordError(err) - logger := log.New("resource-permissions-api") if errors.Is(err, ErrRestConfigNotAvailable) { - logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) } else { - logger.Warn("Failed to get resource permissions from k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + a.logger.Warn("Failed to get resource permissions from k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) } } @@ -304,6 +320,19 @@ func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } + if a.shouldUseK8sAPIs() { + err := a.setUserPermissionToK8s(c.Req.Context(), c.Namespace, resourceID, userID, cmd.Permission) + if err == nil { + return permissionSetResponse(cmd) + } + span.RecordError(err) + if errors.Is(err, ErrRestConfigNotAvailable) { + a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } else { + a.logger.Warn("Failed to set user permission in k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } + } + _, err = a.service.SetUserPermission(c.Req.Context(), c.GetOrgID(), accesscontrol.User{ID: userID}, resourceID, cmd.Permission) if err != nil { return response.Err(err) @@ -361,6 +390,19 @@ func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } + if a.shouldUseK8sAPIs() { + err := a.setTeamPermissionToK8s(c.Req.Context(), c.Namespace, resourceID, teamID, cmd.Permission) + if err == nil { + return permissionSetResponse(cmd) + } + span.RecordError(err) + if errors.Is(err, ErrRestConfigNotAvailable) { + a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } else { + a.logger.Warn("Failed to set team permission in k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } + } + _, err = a.service.SetTeamPermission(c.Req.Context(), c.GetOrgID(), teamID, resourceID, cmd.Permission) if err != nil { return response.Err(err) @@ -415,6 +457,17 @@ func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Resp return response.Error(http.StatusBadRequest, "bad request data", err) } + if a.shouldUseK8sAPIs() { + err := a.setBuiltInRolePermissionToK8s(c.Req.Context(), c.Namespace, resourceID, builtInRole, cmd.Permission) + if err == nil { + return permissionSetResponse(cmd) + } + span.RecordError(err) + if errors.Is(err, ErrRestConfigNotAvailable) { + a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } + } + _, err := a.service.SetBuiltInRolePermission(c.Req.Context(), c.GetOrgID(), builtInRole, resourceID, cmd.Permission) if err != nil { return response.Err(err) @@ -463,6 +516,17 @@ func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "Bad request data: "+err.Error(), err) } + if a.shouldUseK8sAPIs() { + err := a.setResourcePermissionsToK8s(c.Req.Context(), c.Namespace, resourceID, cmd.Permissions) + if err == nil { + return response.Success("Permissions updated") + } + span.RecordError(err) + if errors.Is(err, ErrRestConfigNotAvailable) { + a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource) + } + } + _, err := a.service.SetPermissions(ctx, c.GetOrgID(), resourceID, cmd.Permissions...) if err != nil { return response.Err(err) diff --git a/pkg/services/accesscontrol/resourcepermissions/api_adapter.go b/pkg/services/accesscontrol/resourcepermissions/api_adapter.go index 868ae1a32b5..578111368e5 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_adapter.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_adapter.go @@ -13,9 +13,12 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" ) @@ -162,3 +165,231 @@ func getMapKeys(m map[string][]string) []string { func (a *api) buildResourcePermissionName(resourceID string) string { return fmt.Sprintf("%s-%s-%s", a.getAPIGroup(), a.service.options.Resource, resourceID) } + +// Write operations + +func (a *api) setResourcePermissionsToK8s(ctx context.Context, namespace string, resourceID string, permissions []accesscontrol.SetResourcePermissionCommand) error { + dynamicClient, err := a.getDynamicClient(ctx) + if err != nil { + return err + } + + resourcePermName := a.buildResourcePermissionName(resourceID) + resourcePermResource := dynamicClient.Resource(iamv0.ResourcePermissionInfo.GroupVersionResource()).Namespace(namespace) + + _, existingResourceVersion, err := a.getExistingResourcePermission(ctx, resourcePermResource, resourcePermName) + if err != nil { + return err + } + + k8sPermissions := make([]iamv0.ResourcePermissionspecPermission, 0, len(permissions)) + for _, perm := range permissions { + if perm.Permission == "" { + continue + } + + kind := a.getPermissionKind(perm) + name, err := a.getPermissionName(ctx, perm) + if err != nil { + return fmt.Errorf("failed to get permission name: %w", err) + } + + k8sPermissions = append(k8sPermissions, iamv0.ResourcePermissionspecPermission{ + Kind: iamv0.ResourcePermissionSpecPermissionKind(kind), + Name: name, + Verb: cases.Lower(language.Und).String(perm.Permission), + }) + } + + if len(k8sPermissions) == 0 { + if existingResourceVersion != "" { + err = resourcePermResource.Delete(ctx, resourcePermName, metav1.DeleteOptions{}) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("failed to delete resource permission in k8s: %w", err) + } + } + return nil + } + + resourcePerm := &iamv0.ResourcePermission{ + TypeMeta: metav1.TypeMeta{ + APIVersion: iamv0.ResourcePermissionInfo.GroupVersion().String(), + Kind: iamv0.ResourcePermissionInfo.TypeMeta().Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePermName, + Namespace: namespace, + ResourceVersion: existingResourceVersion, + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: a.getAPIGroup(), + Resource: a.service.options.Resource, + Name: resourceID, + }, + Permissions: k8sPermissions, + }, + } + + return a.createOrUpdateResourcePermission(ctx, resourcePermResource, resourcePerm, existingResourceVersion != "") +} + +func (a *api) setUserPermissionToK8s(ctx context.Context, namespace string, resourceID string, userID int64, permission string) error { + userDetails, err := a.service.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userID}) + if err != nil { + return fmt.Errorf("failed to get user details: %w", err) + } + + return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindUser), userDetails.UID, permission) +} + +func (a *api) setTeamPermissionToK8s(ctx context.Context, namespace string, resourceID string, teamID int64, permission string) error { + teamDetails, err := a.service.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{ID: teamID}) + if err != nil { + return fmt.Errorf("failed to get team details: %w", err) + } + + return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindTeam), teamDetails.UID, permission) +} + +func (a *api) setBuiltInRolePermissionToK8s(ctx context.Context, namespace string, resourceID string, builtInRole string, permission string) error { + return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindBasicRole), builtInRole, permission) +} + +func (a *api) setSinglePermissionToK8s(ctx context.Context, namespace string, resourceID string, kind string, name string, permission string) error { + dynamicClient, err := a.getDynamicClient(ctx) + if err != nil { + return err + } + + resourcePermName := a.buildResourcePermissionName(resourceID) + resourcePermResource := dynamicClient.Resource(iamv0.ResourcePermissionInfo.GroupVersionResource()).Namespace(namespace) + + existingResourcePerm, existingResourceVersion, err := a.getExistingResourcePermission(ctx, resourcePermResource, resourcePermName) + if err != nil { + return err + } + + newPermissions := make([]iamv0.ResourcePermissionspecPermission, 0) + for _, perm := range existingResourcePerm.Spec.Permissions { + if string(perm.Kind) == kind && perm.Name == name { + continue + } + newPermissions = append(newPermissions, perm) + } + + if permission != "" { + newPermissions = append(newPermissions, iamv0.ResourcePermissionspecPermission{ + Kind: iamv0.ResourcePermissionSpecPermissionKind(kind), + Name: name, + Verb: cases.Lower(language.Und).String(permission), + }) + } + + if len(newPermissions) == 0 { + if existingResourceVersion != "" { + err = resourcePermResource.Delete(ctx, resourcePermName, metav1.DeleteOptions{}) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("failed to delete resource permission in k8s: %w", err) + } + } + return nil + } + + resourcePerm := &iamv0.ResourcePermission{ + TypeMeta: metav1.TypeMeta{ + APIVersion: iamv0.ResourcePermissionInfo.GroupVersion().String(), + Kind: iamv0.ResourcePermissionInfo.TypeMeta().Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePermName, + Namespace: namespace, + ResourceVersion: existingResourceVersion, + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: a.getAPIGroup(), + Resource: a.service.options.Resource, + Name: resourceID, + }, + Permissions: newPermissions, + }, + } + + return a.createOrUpdateResourcePermission(ctx, resourcePermResource, resourcePerm, existingResourceVersion != "") +} + +func (a *api) getPermissionKind(perm accesscontrol.SetResourcePermissionCommand) string { + if perm.UserID != 0 { + return string(iamv0.ResourcePermissionSpecPermissionKindUser) + } + if perm.TeamID != 0 { + return string(iamv0.ResourcePermissionSpecPermissionKindTeam) + } + if perm.BuiltinRole != "" { + return string(iamv0.ResourcePermissionSpecPermissionKindBasicRole) + } + return "" +} + +func (a *api) getExistingResourcePermission(ctx context.Context, resourcePermResource dynamic.ResourceInterface, resourcePermName string) (*iamv0.ResourcePermission, string, error) { + unstructuredObj, err := resourcePermResource.Get(ctx, resourcePermName, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return &iamv0.ResourcePermission{}, "", nil + } + return nil, "", fmt.Errorf("failed to get existing resource permission: %w", err) + } + + var resourcePerm iamv0.ResourcePermission + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.Object, &resourcePerm); err != nil { + return nil, "", fmt.Errorf("failed to convert existing resource permission: %w", err) + } + + return &resourcePerm, unstructuredObj.GetResourceVersion(), nil +} + +func (a *api) createOrUpdateResourcePermission(ctx context.Context, resourcePermResource dynamic.ResourceInterface, resourcePerm *iamv0.ResourcePermission, isUpdate bool) error { + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resourcePerm) + if err != nil { + return fmt.Errorf("failed to convert resource permission to unstructured: %w", err) + } + unstructuredPerm := &unstructured.Unstructured{Object: unstructuredObj} + + if isUpdate { + _, err = resourcePermResource.Update(ctx, unstructuredPerm, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update resource permission in k8s: %w", err) + } + } else { + _, err = resourcePermResource.Create(ctx, unstructuredPerm, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create resource permission in k8s: %w", err) + } + } + + return nil +} + +func (a *api) getPermissionName(ctx context.Context, perm accesscontrol.SetResourcePermissionCommand) (string, error) { + if perm.UserID != 0 { + userDetails, err := a.service.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: perm.UserID}) + if err != nil { + return "", fmt.Errorf("failed to get user details for user ID %d: %w", perm.UserID, err) + } + return userDetails.UID, nil + } + if perm.TeamID != 0 { + teamDetails, err := a.service.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{ + ID: perm.TeamID, + }) + if err != nil { + return "", fmt.Errorf("failed to get team details for team ID %d: %w", perm.TeamID, err) + } + return teamDetails.UID, nil + } + if perm.BuiltinRole != "" { + return perm.BuiltinRole, nil + } + return "", fmt.Errorf("no valid permission subject found") +} diff --git a/pkg/services/accesscontrol/resourcepermissions/api_adapter_test.go b/pkg/services/accesscontrol/resourcepermissions/api_adapter_test.go new file mode 100644 index 00000000000..ca0ca214c9a --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/api_adapter_test.go @@ -0,0 +1,200 @@ +package resourcepermissions + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +// TestGetPermissionKind tests the permission kind mapping logic +func TestGetPermissionKind(t *testing.T) { + api := &api{ + service: &Service{ + options: Options{ + Resource: "dashboards", + ResourceAttribute: "uid", + }, + }, + } + + tests := []struct { + name string + perm accesscontrol.SetResourcePermissionCommand + expected string + }{ + { + name: "user permission", + perm: accesscontrol.SetResourcePermissionCommand{UserID: 123}, + expected: string(iamv0.ResourcePermissionSpecPermissionKindUser), + }, + { + name: "team permission", + perm: accesscontrol.SetResourcePermissionCommand{TeamID: 456}, + expected: string(iamv0.ResourcePermissionSpecPermissionKindTeam), + }, + { + name: "builtin role permission", + perm: accesscontrol.SetResourcePermissionCommand{BuiltinRole: "Editor"}, + expected: string(iamv0.ResourcePermissionSpecPermissionKindBasicRole), + }, + { + name: "empty permission returns empty kind", + perm: accesscontrol.SetResourcePermissionCommand{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kind := api.getPermissionKind(tt.perm) + assert.Equal(t, tt.expected, kind) + }) + } +} + +// TestGetDynamicClient_RestConfigNotAvailable tests error handling when rest config is not available +func TestGetDynamicClient_RestConfigNotAvailable(t *testing.T) { + ctx := context.Background() + + api := &api{ + service: &Service{ + options: Options{ + Resource: "dashboards", + }, + }, + restConfigProvider: nil, + } + + client, err := api.getDynamicClient(ctx) + + assert.Error(t, err) + assert.Nil(t, client) + assert.Equal(t, ErrRestConfigNotAvailable, err) +} + +// TestBuildResourcePermissionName tests resource permission name building +func TestBuildResourcePermissionName(t *testing.T) { + tests := []struct { + name string + apiGroup string + resource string + resourceID string + expectedName string + }{ + { + name: "with custom API group", + apiGroup: "dashboard.grafana.app", + resource: "dashboards", + resourceID: "dashboard-uid-123", + expectedName: "dashboard.grafana.app-dashboards-dashboard-uid-123", + }, + { + name: "with default API group", + apiGroup: "", + resource: "folders", + resourceID: "folder-uid-456", + expectedName: "folders.grafana.app-folders-folder-uid-456", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &api{ + service: &Service{ + options: Options{ + Resource: tt.resource, + APIGroup: tt.apiGroup, + }, + }, + } + + name := api.buildResourcePermissionName(tt.resourceID) + assert.Equal(t, tt.expectedName, name) + }) + } +} + +// TestGetAPIGroup tests API group resolution +func TestGetAPIGroup(t *testing.T) { + t.Run("returns custom API group when set", func(t *testing.T) { + api := &api{ + service: &Service{ + options: Options{ + Resource: "dashboards", + APIGroup: "custom.grafana.app", + }, + }, + } + + group := api.getAPIGroup() + assert.Equal(t, "custom.grafana.app", group) + }) + + t.Run("returns default API group when not set", func(t *testing.T) { + api := &api{ + service: &Service{ + options: Options{ + Resource: "dashboards", + APIGroup: "", + }, + }, + } + + group := api.getAPIGroup() + assert.Equal(t, "dashboards.grafana.app", group) + }) + + t.Run("default group for folders", func(t *testing.T) { + api := &api{ + service: &Service{ + options: Options{ + Resource: "folders", + APIGroup: "", + }, + }, + } + + group := api.getAPIGroup() + assert.Equal(t, "folders.grafana.app", group) + }) +} + +// TestResourcePermissionKindConstants verifies the kind constants match expected values +func TestResourcePermissionKindConstants(t *testing.T) { + tests := []struct { + name string + kind iamv0.ResourcePermissionSpecPermissionKind + expected string + }{ + { + name: "User kind", + kind: iamv0.ResourcePermissionSpecPermissionKindUser, + expected: "User", + }, + { + name: "Team kind", + kind: iamv0.ResourcePermissionSpecPermissionKindTeam, + expected: "Team", + }, + { + name: "ServiceAccount kind", + kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, + expected: "ServiceAccount", + }, + { + name: "BasicRole kind", + kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, + expected: "BasicRole", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, string(tt.kind)) + }) + } +} diff --git a/pkg/services/accesscontrol/seeding/seeder.go b/pkg/services/accesscontrol/seeding/seeder.go new file mode 100644 index 00000000000..cad31b7a5d2 --- /dev/null +++ b/pkg/services/accesscontrol/seeding/seeder.go @@ -0,0 +1,451 @@ +package seeding + +import ( + "context" + "fmt" + "regexp" + "slices" + "strings" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" +) + +type Seeder struct { + log log.Logger + roleStore accesscontrol.RoleStore + backend SeedingBackend + builtinsPermissions map[accesscontrol.SeedPermission]struct{} + seededFixedRoles map[string]bool + seededPluginRoles map[string]bool + seededPlugins map[string]bool + hasSeededAlready bool +} + +// SeedingBackend provides the seed-set specific operations needed to seed. +type SeedingBackend interface { + // LoadPrevious returns the currently stored permissions for previously seeded roles. + LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) + + // Apply updates the database to match the desired permissions. + Apply(ctx context.Context, + added, removed []accesscontrol.SeedPermission, + updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, + ) error +} + +func New(log log.Logger, roleStore accesscontrol.RoleStore, backend SeedingBackend) *Seeder { + return &Seeder{ + log: log, + roleStore: roleStore, + backend: backend, + builtinsPermissions: map[accesscontrol.SeedPermission]struct{}{}, + seededFixedRoles: map[string]bool{}, + seededPluginRoles: map[string]bool{}, + seededPlugins: map[string]bool{}, + hasSeededAlready: false, + } +} + +// SetDesiredPermissions replaces the in-memory desired permission set used by Seed(). +func (s *Seeder) SetDesiredPermissions(desired map[accesscontrol.SeedPermission]struct{}) { + if desired == nil { + s.builtinsPermissions = map[accesscontrol.SeedPermission]struct{}{} + return + } + s.builtinsPermissions = desired +} + +// Seed loads current and desired permissions, diffs them (including scope updates), applies changes, and bumps versions. +func (s *Seeder) Seed(ctx context.Context) error { + previous, err := s.backend.LoadPrevious(ctx) + if err != nil { + return err + } + + // - Do not remove plugin permissions when the plugin didn't register this run (Origin set but not in seededPlugins). + // - Preserve legacy plugin app access permissions in the persisted seed set (these are granted by default). + if len(previous) > 0 { + filtered := make(map[accesscontrol.SeedPermission]struct{}, len(previous)) + for p := range previous { + if p.Action == pluginaccesscontrol.ActionAppAccess { + continue + } + if p.Origin != "" && !s.seededPlugins[p.Origin] { + continue + } + filtered[p] = struct{}{} + } + previous = filtered + } + + added, removed, updated := s.permissionDiff(previous, s.builtinsPermissions) + + if err := s.backend.Apply(ctx, added, removed, updated); err != nil { + return err + } + return nil +} + +// SeedRoles populates the database with the roles and their assignments +// It will create roles that do not exist and update roles that have changed +// Do not use for provisioning. Validation is not enforced. +func (s *Seeder) SeedRoles(ctx context.Context, registrationList []accesscontrol.RoleRegistration) error { + roleMap, err := s.roleStore.LoadRoles(ctx) + if err != nil { + return err + } + + missingRoles := make([]accesscontrol.RoleRegistration, 0, len(registrationList)) + + // Diff existing roles with the ones we want to seed. + // If a role is missing, we add it to the missingRoles list + for _, registration := range registrationList { + registration := registration + role, ok := roleMap[registration.Role.Name] + switch { + case registration.Role.IsFixed(): + s.seededFixedRoles[registration.Role.Name] = true + case registration.Role.IsPlugin(): + s.seededPluginRoles[registration.Role.Name] = true + // To be resilient to failed plugin loadings, we remember the plugins that have registered, + // later we'll ignore permissions and roles of other plugins + s.seededPlugins[pluginutils.PluginIDFromName(registration.Role.Name)] = true + } + + s.rememberPermissionAssignments(®istration.Role, registration.Grants, registration.Exclude) + + if !ok { + missingRoles = append(missingRoles, registration) + continue + } + + if needsRoleUpdate(role, registration.Role) { + if err := s.roleStore.SetRole(ctx, role, registration.Role); err != nil { + return err + } + } + + if needsPermissionsUpdate(role, registration.Role) { + if err := s.roleStore.SetPermissions(ctx, role, registration.Role); err != nil { + return err + } + } + } + + for _, registration := range missingRoles { + if err := s.roleStore.CreateRole(ctx, registration.Role); err != nil { + return err + } + } + + return nil +} + +func needsPermissionsUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool { + if existingRole == nil { + return true + } + + if len(existingRole.Permissions) != len(wantedRole.Permissions) { + return true + } + + for _, p := range wantedRole.Permissions { + found := false + for _, ep := range existingRole.Permissions { + if ep.Action == p.Action && ep.Scope == p.Scope { + found = true + break + } + } + if !found { + return true + } + } + + return false +} + +func needsRoleUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool { + if existingRole == nil { + return true + } + + if existingRole.Name != wantedRole.Name { + return false + } + + if existingRole.DisplayName != wantedRole.DisplayName { + return true + } + + if existingRole.Description != wantedRole.Description { + return true + } + + if existingRole.Group != wantedRole.Group { + return true + } + + if existingRole.Hidden != wantedRole.Hidden { + return true + } + + return false +} + +// Deprecated: SeedRole is deprecated and should not be used. +// SeedRoles only does boot up seeding and should not be used for runtime seeding. +func (s *Seeder) SeedRole(ctx context.Context, role accesscontrol.RoleDTO, builtInRoles []string) error { + addedPermissions := make(map[string]struct{}, len(role.Permissions)) + permissions := make([]accesscontrol.Permission, 0, len(role.Permissions)) + for _, p := range role.Permissions { + key := fmt.Sprintf("%s:%s", p.Action, p.Scope) + if _, ok := addedPermissions[key]; !ok { + addedPermissions[key] = struct{}{} + permissions = append(permissions, accesscontrol.Permission{Action: p.Action, Scope: p.Scope}) + } + } + + wantedRole := accesscontrol.RoleDTO{ + OrgID: accesscontrol.GlobalOrgID, + Version: role.Version, + UID: role.UID, + Name: role.Name, + DisplayName: role.DisplayName, + Description: role.Description, + Group: role.Group, + Permissions: permissions, + Hidden: role.Hidden, + } + roleMap, err := s.roleStore.LoadRoles(ctx) + if err != nil { + return err + } + + existingRole := roleMap[wantedRole.Name] + if existingRole == nil { + if err := s.roleStore.CreateRole(ctx, wantedRole); err != nil { + return err + } + } else { + if needsRoleUpdate(existingRole, wantedRole) { + if err := s.roleStore.SetRole(ctx, existingRole, wantedRole); err != nil { + return err + } + } + if needsPermissionsUpdate(existingRole, wantedRole) { + if err := s.roleStore.SetPermissions(ctx, existingRole, wantedRole); err != nil { + return err + } + } + } + + // Remember seeded roles + if wantedRole.IsFixed() { + s.seededFixedRoles[wantedRole.Name] = true + } + isPluginRole := wantedRole.IsPlugin() + if isPluginRole { + s.seededPluginRoles[wantedRole.Name] = true + + // To be resilient to failed plugin loadings, we remember the plugins that have registered, + // later we'll ignore permissions and roles of other plugins + s.seededPlugins[pluginutils.PluginIDFromName(role.Name)] = true + } + + s.rememberPermissionAssignments(&wantedRole, builtInRoles, []string{}) + return nil +} + +func (s *Seeder) rememberPermissionAssignments(role *accesscontrol.RoleDTO, builtInRoles []string, excludedRoles []string) { + AppendDesiredPermissions(s.builtinsPermissions, s.log, role, builtInRoles, excludedRoles, true) +} + +// AppendDesiredPermissions accumulates permissions from a role registration onto basic roles (Viewer/Editor/Admin/Grafana Admin). +// - It expands parents via accesscontrol.BuiltInRolesWithParents. +// - It can optionally ignore plugin app access permissions (which are granted by default). +func AppendDesiredPermissions( + out map[accesscontrol.SeedPermission]struct{}, + logger log.Logger, + role *accesscontrol.RoleDTO, + builtInRoles []string, + excludedRoles []string, + ignorePluginAppAccess bool, +) { + if out == nil || role == nil { + return + } + + for builtInRole := range accesscontrol.BuiltInRolesWithParents(builtInRoles) { + // Skip excluded grants + if slices.Contains(excludedRoles, builtInRole) { + continue + } + + for _, perm := range role.Permissions { + if ignorePluginAppAccess && perm.Action == pluginaccesscontrol.ActionAppAccess { + logger.Debug("Role is attempting to grant access permission, but this permission is already granted by default and will be ignored", + "role", role.Name, "permission", perm.Action, "scope", perm.Scope) + continue + } + + sp := accesscontrol.SeedPermission{ + BuiltInRole: builtInRole, + Action: perm.Action, + Scope: perm.Scope, + } + + if role.IsPlugin() { + sp.Origin = pluginutils.PluginIDFromName(role.Name) + } + + out[sp] = struct{}{} + } + } +} + +// permissionDiff returns: +// - added: present in desired permissions, not in previous permissions +// - removed: present in previous permissions, not in desired permissions +// - updated: same role + action, but scope changed +func (s *Seeder) permissionDiff(previous, desired map[accesscontrol.SeedPermission]struct{}) (added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) { + addedSet := make(map[accesscontrol.SeedPermission]struct{}, 0) + for n := range desired { + if _, already := previous[n]; !already { + addedSet[n] = struct{}{} + } else { + delete(previous, n) + } + } + + // Check if any of the new permissions is actually an old permission with an updated scope + updated = make(map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, 0) + for n := range addedSet { + for p := range previous { + if n.BuiltInRole == p.BuiltInRole && n.Action == p.Action { + updated[p] = n + delete(addedSet, n) + } + } + } + + for p := range addedSet { + added = append(added, p) + } + + for p := range previous { + if p.Action == pluginaccesscontrol.ActionAppAccess && + p.Scope != pluginaccesscontrol.ScopeProvider.GetResourceAllScope() { + // Allows backward compatibility with plugins that have been seeded before the grant ignore rule was added + s.log.Info("This permission already existed so it will not be removed", + "role", p.BuiltInRole, "permission", p.Action, "scope", p.Scope) + continue + } + + removed = append(removed, p) + } + + return added, removed, updated +} + +func (s *Seeder) ClearBasicRolesPluginPermissions(ID string) { + removable := []accesscontrol.SeedPermission{} + + for key := range s.builtinsPermissions { + if matchPermissionByPluginID(key, ID) { + removable = append(removable, key) + } + } + + for _, perm := range removable { + delete(s.builtinsPermissions, perm) + } +} + +func matchPermissionByPluginID(perm accesscontrol.SeedPermission, pluginID string) bool { + if perm.Origin != pluginID { + return false + } + actionTemplate := regexp.MustCompile(fmt.Sprintf("%s[.:]", pluginID)) + scopeTemplate := fmt.Sprintf(":%s", pluginID) + return actionTemplate.MatchString(perm.Action) || strings.HasSuffix(perm.Scope, scopeTemplate) +} + +// RolesToUpgrade returns the unique basic roles that should have their version incremented. +func RolesToUpgrade(added, removed []accesscontrol.SeedPermission) []string { + set := map[string]struct{}{} + for _, p := range added { + set[p.BuiltInRole] = struct{}{} + } + for _, p := range removed { + set[p.BuiltInRole] = struct{}{} + } + out := make([]string, 0, len(set)) + for r := range set { + out = append(out, r) + } + return out +} + +func (s *Seeder) ClearPluginRoles(ID string) { + expectedPrefix := fmt.Sprintf("%s%s:", accesscontrol.PluginRolePrefix, ID) + + for roleName := range s.seededPluginRoles { + if strings.HasPrefix(roleName, expectedPrefix) { + delete(s.seededPluginRoles, roleName) + } + } +} + +func (s *Seeder) MarkSeededAlready() { + s.hasSeededAlready = true +} + +func (s *Seeder) HasSeededAlready() bool { + return s.hasSeededAlready +} + +func (s *Seeder) RemoveAbsentRoles(ctx context.Context) error { + roleMap, errGet := s.roleStore.LoadRoles(ctx) + if errGet != nil { + s.log.Error("failed to get fixed roles from store", "err", errGet) + return errGet + } + + toRemove := []string{} + for _, r := range roleMap { + if r == nil { + continue + } + if r.IsFixed() { + if !s.seededFixedRoles[r.Name] { + s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name) + toRemove = append(toRemove, r.UID) + } + continue + } + + if r.IsPlugin() { + if !s.seededPlugins[pluginutils.PluginIDFromName(r.Name)] { + // To be resilient to failed plugin loadings + // ignore stored roles related to plugins that have not registered this time + s.log.Debug("plugin role has not been registered on this run skipping its removal", "role", r.Name) + continue + } + if !s.seededPluginRoles[r.Name] { + s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name) + toRemove = append(toRemove, r.UID) + } + } + } + + if errDelete := s.roleStore.DeleteRoles(ctx, toRemove); errDelete != nil { + s.log.Error("failed to delete absent fixed and plugin roles", "err", errDelete) + return errDelete + } + return nil +} diff --git a/pkg/services/apiserver/appinstaller/installer.go b/pkg/services/apiserver/appinstaller/installer.go index b6e9145ff15..d282b42f547 100644 --- a/pkg/services/apiserver/appinstaller/installer.go +++ b/pkg/services/apiserver/appinstaller/installer.go @@ -108,9 +108,14 @@ func RegisterAuthorizers( if authorizerProvider, ok := installer.(AuthorizerProvider); ok { authorizer := authorizerProvider.GetAuthorizer() for _, gv := range installer.GroupVersions() { + if authorizer == nil { + panic("authorizer cannot be nil for api group: " + gv.String()) + } registrar.Register(gv, authorizer) logger.Debug("Registered authorizer", "group", gv.Group, "version", gv.Version, "app") } + } else { + panic("authorizer cannot be nil for api group: " + installer.GroupVersions()[0].Group) } } } diff --git a/pkg/services/apiserver/appinstaller/installer_test.go b/pkg/services/apiserver/appinstaller/installer_test.go index 89c4bfa0164..53c2d2dbedf 100644 --- a/pkg/services/apiserver/appinstaller/installer_test.go +++ b/pkg/services/apiserver/appinstaller/installer_test.go @@ -15,6 +15,7 @@ func TestRegisterAuthorizers(t *testing.T) { name string appInstallers []appsdkapiserver.AppInstaller expectedRegisters int + expectedPanic bool }{ { name: "empty installers list", @@ -30,7 +31,7 @@ func TestRegisterAuthorizers(t *testing.T) { }, }, }, - expectedRegisters: 0, + expectedPanic: true, }, { name: "single installer with authorizer provider", @@ -46,6 +47,20 @@ func TestRegisterAuthorizers(t *testing.T) { }, expectedRegisters: 1, }, + { + name: "single installer with invalid authorizer provider", + appInstallers: []appsdkapiserver.AppInstaller{ + &mockAppInstallerWithAuth{ + mockAppInstaller: &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "test.example.com", Version: "v1"}, + }, + }, + mockAuthorizer: nil, + }, + }, + expectedPanic: true, + }, { name: "installer with multiple group versions", appInstallers: []appsdkapiserver.AppInstaller{ @@ -63,7 +78,7 @@ func TestRegisterAuthorizers(t *testing.T) { expectedRegisters: 3, }, { - name: "multiple installers with mixed authorizer support", + name: "multiple installers with authorizer support", appInstallers: []appsdkapiserver.AppInstaller{ &mockAppInstallerWithAuth{ mockAppInstaller: &mockAppInstaller{ @@ -73,11 +88,6 @@ func TestRegisterAuthorizers(t *testing.T) { }, mockAuthorizer: &mockAuthorizer{}, }, - &mockAppInstaller{ - groupVersions: []schema.GroupVersion{ - {Group: "other.example.com", Version: "v1"}, - }, - }, &mockAppInstallerWithAuth{ mockAppInstaller: &mockAppInstaller{ groupVersions: []schema.GroupVersion{ @@ -88,7 +98,7 @@ func TestRegisterAuthorizers(t *testing.T) { mockAuthorizer: &mockAuthorizer{}, }, }, - expectedRegisters: 3, // 1 from first installer + 2 from third installer + expectedRegisters: 3, // 1 from first installer + 2 from second installer }, } @@ -96,6 +106,13 @@ func TestRegisterAuthorizers(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() registrar := &mockAuthorizerRegistrar{} + if tt.expectedPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("%s case did not panic as expected", t.Name()) + } + }() + } RegisterAuthorizers(ctx, tt.appInstallers, registrar) require.Equal(t, tt.expectedRegisters, len(registrar.registrations)) }) diff --git a/pkg/services/apiserver/auth/authorizer/authorizer.go b/pkg/services/apiserver/auth/authorizer/authorizer.go index 54ea8c081d7..dab8167deb0 100644 --- a/pkg/services/apiserver/auth/authorizer/authorizer.go +++ b/pkg/services/apiserver/auth/authorizer/authorizer.go @@ -38,11 +38,13 @@ func NewGrafanaBuiltInSTAuthorizer(cfg *setting.Cfg) *GrafanaAuthorizer { // Individual services may have explicit implementations apis := make(map[string]authorizer.Authorizer) + // The apiVersion flavors will run first and can return early when FGAC has appropriate rules authorizers = append(authorizers, &authorizerForAPI{apis}) - // org role is last -- and will return allow for verbs that match expectations - // The apiVersion flavors will run first and can return early when FGAC has appropriate rules - authorizers = append(authorizers, newRoleAuthorizer()) + // org role authorizer is last -- and will return allow for verbs that match expectations + // it is only helpful here for remote APIs in some cloud use-cases. + //nolint:staticcheck // remove once build handler chains are untangled between local and remote APIs handling + authorizers = append(authorizers, NewRoleAuthorizer()) return &GrafanaAuthorizer{ apis: apis, auth: union.New(authorizers...), diff --git a/pkg/services/apiserver/auth/authorizer/resource.go b/pkg/services/apiserver/auth/authorizer/resource.go index b86f6b40f09..f2f959f9381 100644 --- a/pkg/services/apiserver/auth/authorizer/resource.go +++ b/pkg/services/apiserver/auth/authorizer/resource.go @@ -9,13 +9,13 @@ import ( claims "github.com/grafana/authlib/types" ) -func NewResourceAuthorizer(c claims.AccessClient) authorizer.Authorizer { +func NewResourceAuthorizer(c claims.AccessChecker) authorizer.Authorizer { return ResourceAuthorizer{c} } // ResourceAuthorizer is used to translate authorizer.Authorizer calls to claims.AccessClient calls type ResourceAuthorizer struct { - c claims.AccessClient + c claims.AccessChecker } func (r ResourceAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { diff --git a/pkg/services/apiserver/auth/authorizer/role.go b/pkg/services/apiserver/auth/authorizer/role.go index 39b3b440665..63313352571 100644 --- a/pkg/services/apiserver/auth/authorizer/role.go +++ b/pkg/services/apiserver/auth/authorizer/role.go @@ -15,11 +15,14 @@ var _ authorizer.Authorizer = &roleAuthorizer{} var orgRoleNoneAsViewerAPIGroups = []string{ "productactivation.ext.grafana.com", + // playlist can be removed after this issue is resolved: https://github.com/grafana/grafana/issues/115712 + "playlist.grafana.app", } type roleAuthorizer struct{} -func newRoleAuthorizer() *roleAuthorizer { +// Deprecated: NewRoleAuthorizer exists for apps that were launched with simplistic authorization requirements. Consider using NewResourceAuthorizer instead. +func NewRoleAuthorizer() *roleAuthorizer { return &roleAuthorizer{} } diff --git a/pkg/services/apiserver/builder/common.go b/pkg/services/apiserver/builder/common.go index bebbad8e8a6..e5e46a3340d 100644 --- a/pkg/services/apiserver/builder/common.go +++ b/pkg/services/apiserver/builder/common.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" @@ -59,6 +60,13 @@ type APIGroupAuthorizer interface { GetAuthorizer() authorizer.Authorizer } +// APIGroupAuditor allows different API groups to opt-in and provide their own auditing policy evaluator function. +// Auditing is only enabled if this is implemented. If no customization is needed, you can use the default evaluator, +// `pkg/apiserver/auditing.NewDefaultGrafanaPolicyRuleEvaluator()`. +type APIGroupAuditor interface { + GetPolicyRuleEvaluator() audit.PolicyRuleEvaluator +} + type APIGroupMutation interface { // Mutate allows the builder to make changes to the object before it is persisted. // Context is used only for timeout/deadline/cancellation and tracing information. diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index a76a01dffba..c535443a91e 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/common" + "github.com/grafana/grafana/pkg/apiserver/auditing" "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -497,6 +498,32 @@ func AddPostStartHooks( return nil } +func EvaluatorPolicyRuleFromBuilders(builders []APIGroupBuilder) auditing.PolicyRuleEvaluators { + policyRuleEvaluators := make(auditing.PolicyRuleEvaluators, 0) + + for _, b := range builders { + auditor, ok := b.(APIGroupAuditor) + if !ok { + continue + } + + policyRuleEvaluator := auditor.GetPolicyRuleEvaluator() + if policyRuleEvaluator == nil { + continue + } + + for _, gv := range GetGroupVersions(b) { + if gv.Empty() { + continue + } + + policyRuleEvaluators[gv] = policyRuleEvaluator + } + } + + return policyRuleEvaluators +} + func allowRegisteringResourceByInfo(allowedResources []string, name string) bool { // trim any subresources from the name name = strings.Split(name, "/")[0] diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 605016c112d..6c92350ec2a 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/audit" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/responsewriter" genericapiserver "k8s.io/apiserver/pkg/server" @@ -27,6 +28,7 @@ import ( dataplaneaggregator "github.com/grafana/grafana/pkg/aggregator/apiserver" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/auditing" grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" @@ -113,6 +115,9 @@ type service struct { appInstallers []appsdkapiserver.AppInstaller builderMetrics *builder.BuilderMetrics dualWriterMetrics *grafanarest.DualWriterMetrics + + auditBackend audit.Backend + auditPolicyRuleProvider auditing.PolicyRuleProvider } func ProvideService( @@ -137,6 +142,8 @@ func ProvideService( aggregatorRunner aggregatorrunner.AggregatorRunner, appInstallers []appsdkapiserver.AppInstaller, builderMetrics *builder.BuilderMetrics, + auditBackend audit.Backend, + auditPolicyRuleProvider auditing.PolicyRuleProvider, ) (*service, error) { scheme := builder.ProvideScheme() codecs := builder.ProvideCodecFactory(scheme) @@ -167,6 +174,8 @@ func ProvideService( appInstallers: appInstallers, builderMetrics: builderMetrics, dualWriterMetrics: grafanarest.NewDualWriterMetrics(reg), + auditBackend: auditBackend, + auditPolicyRuleProvider: auditPolicyRuleProvider, } // This will be used when running as a dskit service s.NamedService = services.NewBasicService(s.start, s.running, nil).WithName(modules.GrafanaAPIServer) @@ -275,6 +284,8 @@ func (s *service) start(ctx context.Context) error { auth := a.GetAuthorizer() if auth != nil { s.authorizer.Register(gv, auth) + } else { + panic("authorizer can not be nil for api group=" + gv.String()) } } } @@ -353,6 +364,10 @@ func (s *service) start(ctx context.Context) error { appinstaller.BuildOpenAPIDefGetter(s.appInstallers), } + // Auditing Options + serverConfig.AuditBackend = s.auditBackend + serverConfig.AuditPolicyRuleEvaluator = s.auditPolicyRuleProvider.PolicyRuleProvider(builder.EvaluatorPolicyRuleFromBuilders(s.builders)) + // Add OpenAPI specs for each group+version (existing builders) err = builder.SetupConfig( s.scheme, diff --git a/pkg/services/auth/auth.go b/pkg/services/auth/auth.go index cc678914b31..76b60b68517 100644 --- a/pkg/services/auth/auth.go +++ b/pkg/services/auth/auth.go @@ -20,9 +20,10 @@ const ( // Typed errors var ( - ErrUserTokenNotFound = errors.New("user token not found") - ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken - ErrExternalSessionNotFound = errors.New("external session not found") + ErrUserTokenNotFound = errors.New("user token not found") + ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken + ErrExternalSessionNotFound = errors.New("external session not found") + ErrExternalSessionTokenNotFound = errors.New("session token was nil") ) type ( diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index cb8099ba1d1..ada03081add 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -77,6 +77,10 @@ var ( "user.sync.user-externalUID-mismatch", errutil.WithPublicMessage("User externalUID mismatch"), ) + errSCIMAuthModuleMismatch = errutil.Unauthorized( + "user.sync.scim-auth-module-mismatch", + errutil.WithPublicMessage("User was provisioned via SCIM and must login via SAML"), + ) ) var ( @@ -308,6 +312,21 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth // just try to fetch the user one more to make the other request work. if errors.Is(err, user.ErrUserAlreadyExists) { usr, _, err = s.getUser(ctx, id) + + // Check if this is a SCIM-provisioned user trying to login via an auth module that is not SAML or GCOM + if err == nil && usr != nil && usr.IsProvisioned && id.AuthenticatedBy != login.GrafanaComAuthModule { + _, authErr := s.authInfoService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{ + UserId: usr.ID, + AuthModule: id.AuthenticatedBy, + }) + if errors.Is(authErr, user.ErrUserNotFound) { + s.log.FromContext(ctx).Error("SCIM-provisioned user attempted login via non-SAML auth module", + "user_id", usr.ID, + "attempted_module", id.AuthenticatedBy, + ) + return errSCIMAuthModuleMismatch.Errorf("user was provisioned via SCIM but attempted login via %s", id.AuthenticatedBy) + } + } } if err != nil { diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index dd19836b0a5..ad863164aee 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -1926,3 +1926,100 @@ func TestUserSync_SCIMLoginUsageStatSet(t *testing.T) { finalCount := finalStats["stats.features.scim.has_successful_login.count"].(int) require.Equal(t, int(1), finalCount) } + +func TestUserSync_SyncUserHook_SCIMAuthModuleMismatch(t *testing.T) { + userSrv := usertest.NewMockService(t) + authInfoSrv := authinfotest.NewMockAuthInfoService(t) + + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(nil, user.ErrUserNotFound).Once() + + userSrv.On("Create", mock.Anything, mock.Anything).Return(nil, user.ErrUserAlreadyExists).Once() + + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(&user.User{ + ID: 1, + Email: "test@test.com", + IsProvisioned: true, + }, nil).Once() + + authInfoSrv.On("GetAuthInfo", mock.Anything, mock.MatchedBy(func(q *login.GetAuthInfoQuery) bool { + return q.AuthModule == "oauth_azuread" + })).Return(nil, user.ErrUserNotFound).Once() + + s := ProvideUserSync( + userSrv, + authinfoimpl.ProvideOSSUserProtectionService(), + authInfoSrv, + "atest.FakeQuotaService{}, + tracing.NewNoopTracerService(), + featuremgmt.WithFeatures(), + setting.NewCfg(), + nil, + ) + + email := "test@test.com" + + err := s.SyncUserHook(context.Background(), &authn.Identity{ + AuthenticatedBy: "oauth_azuread", + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + LookUpParams: login.UserLookupParams{ + Email: &email, + }, + }, + }, nil) + + require.Error(t, err) + assert.ErrorIs(t, err, errSCIMAuthModuleMismatch) + assert.Contains(t, err.Error(), "SCIM") + assert.Contains(t, err.Error(), "oauth_azuread") +} + +func TestUserSync_SyncUserHook_SCIMUserAllowsGCOMLogin(t *testing.T) { + userSrv := usertest.NewMockService(t) + authInfoSrv := authinfotest.NewMockAuthInfoService(t) + + authInfoSrv.On("GetAuthInfo", mock.Anything, mock.MatchedBy(func(q *login.GetAuthInfoQuery) bool { + return q.AuthModule == login.GrafanaComAuthModule && q.AuthId == "gcom-user-123" + })).Return(nil, user.ErrUserNotFound).Once() + + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(nil, user.ErrUserNotFound).Once() + userSrv.On("Create", mock.Anything, mock.Anything).Return(nil, user.ErrUserAlreadyExists).Once() + + authInfoSrv.On("GetAuthInfo", mock.Anything, mock.MatchedBy(func(q *login.GetAuthInfoQuery) bool { + return q.AuthModule == login.GrafanaComAuthModule && q.AuthId == "gcom-user-123" + })).Return(nil, user.ErrUserNotFound).Once() + + userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(&user.User{ + ID: 1, + Email: "test@test.com", + IsProvisioned: true, + }, nil).Once() + + s := ProvideUserSync( + userSrv, + authinfoimpl.ProvideOSSUserProtectionService(), + authInfoSrv, + "atest.FakeQuotaService{}, + tracing.NewNoopTracerService(), + featuremgmt.WithFeatures(), + setting.NewCfg(), + nil, + ) + + email := "test@test.com" + + err := s.SyncUserHook(context.Background(), &authn.Identity{ + AuthenticatedBy: login.GrafanaComAuthModule, + AuthID: "gcom-user-123", + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + LookUpParams: login.UserLookupParams{ + Email: &email, + }, + }, + }, nil) + + require.NoError(t, err) +} diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index b479f314c76..be9efa9ce76 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -131,7 +131,8 @@ func (s *ExtendedJWT) authenticateAsUser( return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err) } - if !claims.IsIdentityType(t, claims.TypeUser) { + // TODO: How to support other identity types like render and anonymous here? + if !claims.IsIdentityType(t, claims.TypeUser, claims.TypeServiceAccount) { return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject) } diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 88cf11fb83a..3c7203844b1 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -53,6 +53,17 @@ var ( Namespace: "default", // org ID of 1 is special and translates to default }, } + validIDTokenClaimsWithServiceAccount = idTokenClaims{ + Claims: jwt.Claims{ + Subject: "service-account:3", + Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)), + IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)), + }, + Rest: authnlib.IDTokenClaims{ + AuthenticatedBy: "extended_jwt", + Namespace: "default", // org ID of 1 is special and translates to default + }, + } validIDTokenClaimsWithStackSet = idTokenClaims{ Claims: jwt.Claims{ Subject: "user:2", @@ -118,7 +129,7 @@ var ( } invalidSubjectIDTokenClaims = idTokenClaims{ Claims: jwt.Claims{ - Subject: "service-account:2", + Subject: "anonymous:2", Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)), IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)), }, @@ -286,6 +297,29 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, }, }, + { + name: "should authenticate as service account", + accessToken: &validAccessTokenClaims, + idToken: &validIDTokenClaimsWithServiceAccount, + orgID: 1, + want: &authn.Identity{ + ID: "3", + Type: claims.TypeServiceAccount, + OrgID: 1, + AccessTokenClaims: &validAccessTokenClaims, + IDTokenClaims: &validIDTokenClaimsWithServiceAccount, + Namespace: "default", + AuthenticatedBy: "extendedjwt", + AuthID: "access-policy:this-uid", + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, + SyncPermissions: true, + FetchPermissionsParams: authn.FetchPermissionsParams{ + RestrictedActions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"}, + }, + }, + }, + }, { name: "should authenticate as user in the user namespace", accessToken: &validAccessTokenClaimsWildcard, diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index 23de10511a2..c6c078a9b4a 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -152,7 +152,7 @@ func ProvideStandaloneAuthZClient( //nolint:staticcheck // not yet migrated to OpenFeature zanzanaEnabled := features.IsEnabledGlobally(featuremgmt.FlagZanzana) - zanzanaClient, err := ProvideStandaloneZanzanaClient(cfg, features) + zanzanaClient, err := ProvideStandaloneZanzanaClient(cfg, features, reg) if err != nil { return nil, err } diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index e34965df999..dcf2432fb2c 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -279,8 +279,11 @@ func NewMapperRegistry() MapperRegistry { }, "provisioning.grafana.app": { "repositories": newResourceTranslation("provisioning.repositories", "uid", false, skipScopeOnAllVerbs), + "connections": newResourceTranslation("provisioning.connections", "uid", false, skipScopeOnAllVerbs), "jobs": newResourceTranslation("provisioning.jobs", "uid", false, skipScopeOnAllVerbs), "historicjobs": newResourceTranslation("provisioning.historicjobs", "uid", false, skipScopeOnAllVerbs), + "settings": newResourceTranslation("provisioning.settings", "", false, skipScopeOnAllVerbs), + "stats": newResourceTranslation("provisioning.stats", "", false, skipScopeOnAllVerbs), }, "secret.grafana.app": { "securevalues": newResourceTranslation("secret.securevalues", "uid", false, nil), @@ -298,8 +301,13 @@ func NewMapperRegistry() MapperRegistry { }, }, "plugins.grafana.app": { - "plugins": newResourceTranslation("plugins.plugins", "uid", false, nil), - "pluginsmeta": newResourceTranslation("plugins.pluginsmeta", "uid", false, nil), + "plugins": newResourceTranslation("plugins.plugins", "uid", false, nil), + "metas": newResourceTranslation("plugins.metas", "uid", false, nil), + }, + "advisor.grafana.app": { + "checks": newResourceTranslation("advisor.checks", "uid", false, nil), + "checktypes": newResourceTranslation("advisor.checktypes", "uid", false, nil), + "register": newResourceTranslation("advisor.register", "uid", false, nil), }, }) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index da77010d0eb..79eb19a8b5b 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -4,16 +4,19 @@ import ( "context" "errors" "fmt" + "time" "github.com/fullstorydev/grpchan/inprocgrpc" authnlib "github.com/grafana/authlib/authn" authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/grafana/authlib/grpcutils" "github.com/grafana/authlib/types" + "github.com/grafana/dskit/middleware" "github.com/grafana/dskit/services" grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" @@ -43,14 +46,14 @@ func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, fea switch cfg.ZanzanaClient.Mode { case setting.ZanzanaModeClient: - return NewRemoteZanzanaClient( - fmt.Sprintf("stacks-%s", cfg.StackID), - ZanzanaClientConfig{ - URL: cfg.ZanzanaClient.Addr, - Token: cfg.ZanzanaClient.Token, - TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, - ServerCertFile: cfg.ZanzanaClient.ServerCertFile, - }) + zanzanaConfig := ZanzanaClientConfig{ + Addr: cfg.ZanzanaClient.Addr, + Token: cfg.ZanzanaClient.Token, + TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, + TokenNamespace: cfg.ZanzanaClient.TokenNamespace, + ServerCertFile: cfg.ZanzanaClient.ServerCertFile, + } + return NewRemoteZanzanaClient(zanzanaConfig, reg) case setting.ZanzanaModeEmbedded: logger := log.New("zanzana.server") @@ -75,6 +78,9 @@ func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, fea ctx = types.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ Rest: authnlib.AccessTokenClaims{ Namespace: "*", + Permissions: []string{ + zanzana.TokenPermissionUpdate, + }, }, })) return ctx, nil @@ -97,32 +103,33 @@ func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, fea // 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) { +func ProvideStandaloneZanzanaClient(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (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, + Addr: cfg.ZanzanaClient.Addr, Token: cfg.ZanzanaClient.Token, TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, + TokenNamespace: cfg.ZanzanaClient.TokenNamespace, ServerCertFile: cfg.ZanzanaClient.ServerCertFile, } - return NewRemoteZanzanaClient(cfg.ZanzanaClient.TokenNamespace, zanzanaConfig) + return NewRemoteZanzanaClient(zanzanaConfig, reg) } type ZanzanaClientConfig struct { - URL string + Addr string Token string TokenExchangeURL string - ServerCertFile string TokenNamespace string + ServerCertFile string } // NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server. -func NewRemoteZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client, error) { +func NewRemoteZanzanaClient(cfg ZanzanaClientConfig, reg prometheus.Registerer) (zanzana.Client, error) { tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ Token: cfg.Token, TokenExchangeURL: cfg.TokenExchangeURL, @@ -139,18 +146,25 @@ func NewRemoteZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana. } } + authzRequestDuration := promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Name: "authz_zanzana_client_request_duration_seconds", + Help: "Time spent executing requests to zanzana server.", + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 160, + NativeHistogramMinResetDuration: time.Hour, + }, []string{"operation", "status_code"}) + unaryInterceptors, streamInterceptors := instrument(authzRequestDuration, middleware.ReportGRPCStatusOption) + dialOptions := []grpc.DialOption{ grpc.WithTransportCredentials(transportCredentials), grpc.WithPerRPCCredentials( - NewGRPCTokenAuth( - AuthzServiceAudience, - namespace, - tokenClient, - ), + NewGRPCTokenAuth(AuthzServiceAudience, cfg.TokenNamespace, tokenClient), ), + grpc.WithChainUnaryInterceptor(unaryInterceptors...), + grpc.WithChainStreamInterceptor(streamInterceptors...), } - conn, err := grpc.NewClient(cfg.URL, dialOptions...) + conn, err := grpc.NewClient(cfg.Addr, dialOptions...) if err != nil { return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index c17970ca1b3..4e4bbedc0b7 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -4,8 +4,12 @@ import ( "google.golang.org/protobuf/types/known/structpb" authzv1 "github.com/grafana/authlib/authz/proto/v1" + + dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/accesscontrol" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -44,7 +48,8 @@ func getTypeInfo(group, resource string) (typeInfo, bool) { func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo { typ, relations := getTypeAndRelations(r.GetGroup(), r.GetResource()) - return newResource( + + resource := newResource( typ, r.GetGroup(), r.GetResource(), @@ -53,6 +58,19 @@ func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo { r.GetSubresource(), relations, ) + + // Special case for creating folders and resources in the root folder + if r.GetVerb() == utils.VerbCreate { + if resource.IsFolderResource() && resource.name == "" { + resource.name = accesscontrol.GeneralFolderUID + } else if resource.HasFolderSupport() && resource.folder == "" { + resource.folder = accesscontrol.GeneralFolderUID + } + + return resource + } + + return resource } func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo { @@ -164,3 +182,15 @@ func (r ResourceInfo) IsValidRelation(relation string) bool { func (r ResourceInfo) HasSubresource() bool { return r.subresource != "" } + +var resourcesWithFolderSupport = map[string]bool{ + dashboardV1.DashboardResourceInfo.GroupResource().Group: true, +} + +func (r ResourceInfo) HasFolderSupport() bool { + return resourcesWithFolderSupport[r.group] +} + +func (r ResourceInfo) IsFolderResource() bool { + return r.group == folders.FolderResourceInfo.GroupResource().Group +} diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index b1b6499dcd2..38f38fb90a7 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -58,6 +58,13 @@ const ( RelationGetPermissions string = "get_permissions" RelationSetPermissions string = "set_permissions" + RelationCanGet string = "can_get" + RelationCanCreate string = "can_create" + RelationCanUpdate string = "can_update" + RelationCanDelete string = "can_delete" + RelationCanGetPermissions string = "can_get_permissions" + RelationCanSetPermissions string = "can_set_permissions" + RelationSubresourceSetView string = "resource_" + RelationSetView RelationSubresourceSetEdit string = "resource_" + RelationSetEdit RelationSubresourceSetAdmin string = "resource_" + RelationSetAdmin @@ -134,6 +141,26 @@ var RelationToVerbMapping = map[string]string{ RelationSetPermissions: utils.VerbSetPermissions, } +// FolderPermissionRelation returns the optimized folder relation for permission management. +func FolderPermissionRelation(relation string) string { + switch relation { + case RelationGet: + return RelationCanGet + case RelationCreate: + return RelationCanCreate + case RelationUpdate: + return RelationCanUpdate + case RelationDelete: + return RelationCanDelete + case RelationGetPermissions: + return RelationCanGetPermissions + case RelationSetPermissions: + return RelationCanSetPermissions + default: + return relation + } +} + func IsGroupResourceRelation(relation string) bool { return isValidRelation(relation, RelationsGroupResource) } @@ -228,6 +255,9 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf } if name == "*" { + if m.group != "" && m.resource != "" { + return NewGroupResourceTuple(subject, m.relation, m.group, m.resource, m.subresource), true + } return NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true } diff --git a/pkg/services/authz/zanzana/common/tuple_test.go b/pkg/services/authz/zanzana/common/tuple_test.go new file mode 100644 index 00000000000..ecb4d6e9dd1 --- /dev/null +++ b/pkg/services/authz/zanzana/common/tuple_test.go @@ -0,0 +1,89 @@ +package common + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +type translationTestCase struct { + testName string + subject string + action string + kind string + name string + expected *openfgav1.TupleKey +} + +func TestTranslateToResourceTuple(t *testing.T) { + tests := []translationTestCase{ + { + testName: "dashboards:read in folders", + subject: "user:1", + action: "dashboards:read", + kind: "folders", + name: "*", + expected: &openfgav1.TupleKey{ + User: "user:1", + Relation: "get", + Object: "group_resource:dashboard.grafana.app/dashboards", + }, + }, + { + testName: "dashboards:read for all dashboards", + subject: "user:1", + action: "dashboards:read", + kind: "dashboards", + name: "*", + expected: &openfgav1.TupleKey{ + User: "user:1", + Relation: "get", + Object: "group_resource:dashboard.grafana.app/dashboards", + }, + }, + { + testName: "dashboards:read for general folder", + subject: "user:1", + action: "dashboards:read", + kind: "folders", + name: "general", + expected: &openfgav1.TupleKey{ + User: "user:1", + Relation: "resource_get", + Object: "folder:general", + Condition: &openfgav1.RelationshipCondition{ + Name: "subresource_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "subresources": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{structpb.NewStringValue("dashboard.grafana.app/dashboards")}, + }), + }, + }, + }, + }, + }, + { + testName: "folders:read", + subject: "user:1", + action: "folders:read", + kind: "folders", + name: "*", + expected: &openfgav1.TupleKey{ + User: "user:1", + Relation: "get", + Object: "group_resource:folder.grafana.app/folders", + }, + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + tuple, ok := TranslateToResourceTuple(test.subject, test.action, test.kind, test.name) + require.True(t, ok) + require.EqualExportedValues(t, test.expected, tuple) + }) + } +} diff --git a/pkg/services/authz/zanzana/schema/schema_folder.fga b/pkg/services/authz/zanzana/schema/schema_folder.fga index b9b0a842de9..c55d1312f53 100644 --- a/pkg/services/authz/zanzana/schema/schema_folder.fga +++ b/pkg/services/authz/zanzana/schema/schema_folder.fga @@ -4,15 +4,21 @@ type folder relations define parent: [folder] - # Action sets - define view: [user, service-account, team#member, role#assignee] or edit or view from parent - define edit: [user, service-account, team#member, role#assignee] or admin or edit from parent + # Permission levels define admin: [user, service-account, team#member, role#assignee] or admin from parent + define edit: [user, service-account, team#member, role#assignee] or edit from parent + define view: [user, service-account, team#member, role#assignee] or view from parent + define get: [user, service-account, team#member, role#assignee] or get from parent + define create: [user, service-account, team#member, role#assignee] or create from parent + define update: [user, service-account, team#member, role#assignee] or update from parent + define delete: [user, service-account, team#member, role#assignee] or delete from parent + define get_permissions: [user, service-account, team#member, role#assignee] or get_permissions from parent + define set_permissions: [user, service-account, team#member, role#assignee] or set_permissions from parent - define get: [user, service-account, team#member, role#assignee] or view or get from parent - define create: [user, service-account, team#member, role#assignee] or edit or create from parent - define update: [user, service-account, team#member, role#assignee] or edit or update from parent - define delete: [user, service-account, team#member, role#assignee] or edit or delete from parent - - define get_permissions: [user, service-account, team#member, role#assignee] or admin or get_permissions from parent - define set_permissions: [user, service-account, team#member, role#assignee] or admin or set_permissions from parent + # Computed actions + define can_get: admin or edit or view or get + define can_create: admin or edit or create + define can_update: admin or edit or update + define can_delete: admin or edit or delete + define can_get_permissions: admin or get_permissions + define can_set_permissions: admin or set_permissions diff --git a/pkg/services/authz/zanzana/server/auth.go b/pkg/services/authz/zanzana/server/auth.go index f137bdca47f..5ea412c35b8 100644 --- a/pkg/services/authz/zanzana/server/auth.go +++ b/pkg/services/authz/zanzana/server/auth.go @@ -4,7 +4,9 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/setting" + "golang.org/x/exp/slices" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -30,3 +32,20 @@ func authorize(ctx context.Context, namespace string, ss setting.ZanzanaServerSe } return nil } + +func authorizeWrite(ctx context.Context, namespace string, ss setting.ZanzanaServerSettings) error { + if err := authorize(ctx, namespace, ss); err != nil { + return err + } + + c, ok := claims.AuthInfoFrom(ctx) + if !ok { + return status.Errorf(codes.Unauthenticated, "unauthenticated") + } + + if !slices.Contains(c.GetTokenPermissions(), zanzana.TokenPermissionUpdate) { + return status.Errorf(codes.PermissionDenied, "missing token permission %s", zanzana.TokenPermissionUpdate) + } + + return nil +} diff --git a/pkg/services/authz/zanzana/server/server_bench_test.go b/pkg/services/authz/zanzana/server/server_bench_test.go new file mode 100644 index 00000000000..1d76dbea66b --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_bench_test.go @@ -0,0 +1,947 @@ +package server + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + authzv1 "github.com/grafana/authlib/authz/proto/v1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/log" + "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/common" + "github.com/grafana/grafana/pkg/services/authz/zanzana/store" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +const ( + benchNamespace = "default" + + // Folder tree parameters + foldersPerLevel = 3 + folderDepth = 7 + + // Other data generation parameters + numResources = 50000 + numUsers = 1000 + numTeams = 100 + + // Timeout for List operations + listTimeout = 30 * time.Second + + // Resource type constants for benchmarks + benchDashboardGroup = "dashboard.grafana.app" + benchDashboardResource = "dashboards" + benchFolderGroup = "folder.grafana.app" + benchFolderResource = "folders" + + // BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch. + batchCheckSize = 50 +) + +// benchmarkData holds all the generated test data for benchmarks +type benchmarkData struct { + folders []string // folder UIDs + folderDepths map[string]int // folder UID -> depth level + folderParents map[string]string // folder UID -> parent UID + folderDescendants map[string]int // folder UID -> number of descendants (including self) + foldersByDepth [][]string // folders grouped by depth level + resources []string // resource names + resourceFolders map[string]string // resource name -> folder UID + users []string // user identifiers (e.g., "user:1") + teams []string // team identifiers (e.g., "team:1") + + // Pre-computed test scenarios + deepestFolder string // folder at max depth for worst-case tests + midDepthFolder string // folder at depth/2 + shallowFolder string // folder at depth 1 + rootFolder string // root level folder (depth 0) + largestRootFolder string // root folder with most descendants + largestRootDescCount int // number of descendants in largestRootFolder + maxDepth int // maximum depth in the tree +} + +// generateFolderHierarchy creates a balanced tree of folders. +// Each folder has `childrenPerFolder` children, up to `depth` levels deep. +func generateFolderHierarchy(childrenPerFolder, depth int) ([]*openfgav1.TupleKey, *benchmarkData) { + // Calculate total folders: childrenPerFolder + childrenPerFolder^2 + ... + childrenPerFolder^(depth+1) + totalFolders := 0 + levelSize := childrenPerFolder + for d := 0; d <= depth; d++ { + totalFolders += levelSize + levelSize *= childrenPerFolder + } + + data := &benchmarkData{ + folders: make([]string, 0, totalFolders), + folderDepths: make(map[string]int), + folderParents: make(map[string]string), + folderDescendants: make(map[string]int), + } + tuples := make([]*openfgav1.TupleKey, 0, totalFolders) + + folderIdx := 0 + + // Track folders at each level for parent assignment + levelFolders := make([][]string, depth+1) + for i := range levelFolders { + levelFolders[i] = make([]string, 0) + } + + // Create root level folders (depth 0) + for i := 0; i < childrenPerFolder; i++ { + folderUID := fmt.Sprintf("folder-%d", folderIdx) + data.folders = append(data.folders, folderUID) + data.folderDepths[folderUID] = 0 + levelFolders[0] = append(levelFolders[0], folderUID) + folderIdx++ + } + + // Create folders at each subsequent depth level + for d := 1; d <= depth; d++ { + parentFolders := levelFolders[d-1] + + // Each parent gets exactly childrenPerFolder children + for _, parentUID := range parentFolders { + for j := 0; j < childrenPerFolder; j++ { + folderUID := fmt.Sprintf("folder-%d", folderIdx) + + data.folders = append(data.folders, folderUID) + data.folderDepths[folderUID] = d + data.folderParents[folderUID] = parentUID + levelFolders[d] = append(levelFolders[d], folderUID) + + // Create parent relationship tuple + tuples = append(tuples, common.NewFolderParentTuple(folderUID, parentUID)) + folderIdx++ + } + } + } + + // Set reference folders for different depth scenarios + data.rootFolder = levelFolders[0][0] + data.shallowFolder = levelFolders[0][0] + if len(levelFolders[1]) > 0 { + data.shallowFolder = levelFolders[1][0] + } + midDepth := depth / 2 + if len(levelFolders[midDepth]) > 0 { + data.midDepthFolder = levelFolders[midDepth][0] + } + // Deepest folder + if len(levelFolders[depth]) > 0 { + data.deepestFolder = levelFolders[depth][0] + } + + // Calculate descendant counts for each folder (bottom-up) + // Initialize all folders with count of 1 (self) + for _, folder := range data.folders { + data.folderDescendants[folder] = 1 + } + // Process folders from deepest to shallowest, accumulating descendant counts + for d := depth; d >= 0; d-- { + for _, folder := range levelFolders[d] { + if parent, hasParent := data.folderParents[folder]; hasParent { + data.folderDescendants[parent] += data.folderDescendants[folder] + } + } + } + + // Find root folder with most descendants + for _, rootFolder := range levelFolders[0] { + count := data.folderDescendants[rootFolder] + if count > data.largestRootDescCount { + data.largestRootDescCount = count + data.largestRootFolder = rootFolder + } + } + + // Store folders by depth for depth-based testing + data.foldersByDepth = levelFolders + data.maxDepth = depth + + return tuples, data +} + +// generateResources creates resources distributed across folders +func generateResources(data *benchmarkData, numResources int) []*openfgav1.TupleKey { + data.resources = make([]string, numResources) + data.resourceFolders = make(map[string]string, numResources) + + // Distribute resources across folders + for i := 0; i < numResources; i++ { + resourceName := fmt.Sprintf("resource-%d", i) + folderIdx := i % len(data.folders) + folderUID := data.folders[folderIdx] + + data.resources[i] = resourceName + data.resourceFolders[resourceName] = folderUID + } + + // Note: We don't create tuples for resources themselves, + // permissions are assigned to users/teams on folders or directly on resources + return nil +} + +// generateUsers creates user identifiers +func generateUsers(data *benchmarkData, numUsers int) { + data.users = make([]string, numUsers) + for i := 0; i < numUsers; i++ { + data.users[i] = fmt.Sprintf("user:%d", i) + } +} + +// generateTeams creates team identifiers +func generateTeams(data *benchmarkData, numTeams int) { + data.teams = make([]string, numTeams) + for i := 0; i < numTeams; i++ { + data.teams[i] = fmt.Sprintf("team:%d", i) + } +} + +// generatePermissionTuples creates various permission assignments for benchmarking. +// Users are distributed across 7 patterns: global, root folder, mid-depth folder, +// folder-scoped resource, direct resource, team-based, and no permissions. +const numPermissionPatterns = 7 + +func generatePermissionTuples(data *benchmarkData) []*openfgav1.TupleKey { + tuples := make([]*openfgav1.TupleKey, 0) + + // Distribute users across different permission patterns + usersPerPattern := len(data.users) / numPermissionPatterns + + // Pattern 1: Users with GroupResource permission (all access) + // Users 0 to usersPerPattern-1 + for i := 0; i < usersPerPattern; i++ { + tuples = append(tuples, common.NewGroupResourceTuple( + data.users[i], + common.RelationGet, + benchDashboardGroup, + benchDashboardResource, + "", + )) + } + + // Pattern 2: Users with folder-level permission on root folders + // Users usersPerPattern to 2*usersPerPattern-1 + for i := usersPerPattern; i < 2*usersPerPattern; i++ { + folderIdx := (i - usersPerPattern) % len(data.folders) + // Only assign to root-level folders for this pattern + for j := folderIdx; j < len(data.folders); j++ { + if data.folderDepths[data.folders[j]] == 0 { + tuples = append(tuples, common.NewFolderTuple( + data.users[i], + common.RelationSetView, + data.folders[j], + )) + break + } + } + } + + // Pattern 3: Users with folder-level permission on mid-depth folders + // Use relative depth range: 1/3 to 2/3 of max depth + // Use "view" relation which grants get through the optimized schema + minMidDepth := data.maxDepth / 3 + maxMidDepth := 2 * data.maxDepth / 3 + if maxMidDepth < minMidDepth { + maxMidDepth = minMidDepth + } + // Collect folders in the mid-depth range + var midDepthFolders []string + for d := minMidDepth; d <= maxMidDepth; d++ { + if d < len(data.foldersByDepth) { + midDepthFolders = append(midDepthFolders, data.foldersByDepth[d]...) + } + } + // Fall back to root folders if no mid-depth folders exist + if len(midDepthFolders) == 0 { + midDepthFolders = data.foldersByDepth[0] + } + for i := 2 * usersPerPattern; i < 3*usersPerPattern; i++ { + folderIdx := (i - 2*usersPerPattern) % len(midDepthFolders) + tuples = append(tuples, common.NewFolderTuple( + data.users[i], + common.RelationSetView, + midDepthFolders[folderIdx], + )) + } + + // Pattern 4: Users with folder-scoped resource permission + for i := 3 * usersPerPattern; i < 4*usersPerPattern; i++ { + folderIdx := (i - 3*usersPerPattern) % len(data.folders) + tuples = append(tuples, common.NewFolderResourceTuple( + data.users[i], + common.RelationGet, + benchDashboardGroup, + benchDashboardResource, + "", + data.folders[folderIdx], + )) + } + + // Pattern 5: Users with direct resource permission + for i := 4 * usersPerPattern; i < 5*usersPerPattern; i++ { + resourceIdx := (i - 4*usersPerPattern) % len(data.resources) + tuples = append(tuples, common.NewResourceTuple( + data.users[i], + common.RelationGet, + benchDashboardGroup, + benchDashboardResource, + "", + data.resources[resourceIdx], + )) + } + + // Pattern 6: Team memberships and team permissions + // First, add users to teams + for i := 5 * usersPerPattern; i < 6*usersPerPattern && i < len(data.users); i++ { + teamIdx := (i - 5*usersPerPattern) % len(data.teams) + tuples = append(tuples, common.NewTypedTuple( + common.TypeTeam, + data.users[i], + common.RelationTeamMember, + fmt.Sprintf("%d", teamIdx), + )) + } + // Then, give teams folder permissions + // Use "view" relation which grants get through the optimized schema + for i := 0; i < len(data.teams); i++ { + folderIdx := i % len(data.folders) + teamMember := fmt.Sprintf("team:%d#member", i) + tuples = append(tuples, common.NewFolderTuple( + teamMember, + common.RelationSetView, + data.folders[folderIdx], + )) + } + + // Pattern 7: Users with no permissions (remaining users) + // These users don't get any tuples - they're for testing denial cases + + return tuples +} + +// setupBenchmarkServer creates a server with the benchmark data loaded +func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) { + b.Helper() + if testing.Short() { + b.Skip("skipping benchmark in short mode") + } + + cfg := setting.NewCfg() + testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg)) + + openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger()) + require.NoError(b, err) + + openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, openFGAStore) + require.NoError(b, err) + + srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry()) + require.NoError(b, err) + + // Generate test data + b.Log("Generating folder hierarchy...") + folderTuples, data := generateFolderHierarchy(foldersPerLevel, folderDepth) + + b.Log("Generating resources...") + generateResources(data, numResources) + + b.Log("Generating users...") + generateUsers(data, numUsers) + + b.Log("Generating teams...") + generateTeams(data, numTeams) + + b.Log("Generating permission tuples...") + permTuples := generatePermissionTuples(data) + + // Add special user with permission on largest root folder (for >1000 folder test) + // Use "view" relation which grants get through the optimized schema + largeRootUserTuple := common.NewFolderTuple( + "user:large-root-access", + common.RelationSetView, + data.largestRootFolder, + ) + permTuples = append(permTuples, largeRootUserTuple) + + // Add users with permissions at each depth level for depth-based testing + // Use "view" relation which grants get through the optimized schema + for depth := 0; depth <= data.maxDepth; depth++ { + if len(data.foldersByDepth[depth]) == 0 { + continue + } + folder := data.foldersByDepth[depth][0] + user := fmt.Sprintf("user:depth-%d-access", depth) + permTuples = append(permTuples, common.NewFolderTuple(user, common.RelationSetView, folder)) + } + + // Combine all tuples + allTuples := append(folderTuples, permTuples...) + + b.Logf("Total tuples to write: %d", len(allTuples)) + + // Get store info + ctx := newContextWithZanzanaUpdatePermission() + storeInf, err := srv.getStoreInfo(ctx, benchNamespace) + require.NoError(b, err) + + // Write tuples in batches (OpenFGA limits to 100 per write) + batchSize := 100 + for i := 0; i < len(allTuples); i += batchSize { + end := i + batchSize + if end > len(allTuples) { + end = len(allTuples) + } + batch := allTuples[i:end] + + _, err = srv.openfga.Write(ctx, &openfgav1.WriteRequest{ + StoreId: storeInf.ID, + AuthorizationModelId: storeInf.ModelID, + Writes: &openfgav1.WriteRequestWrites{ + TupleKeys: batch, + OnDuplicate: "ignore", + }, + }) + require.NoError(b, err) + + if (i/batchSize)%100 == 0 { + b.Logf("Written %d/%d tuples", end, len(allTuples)) + } + } + + b.Logf("Benchmark data setup complete: %d folders, %d resources, %d users, %d teams", + len(data.folders), len(data.resources), len(data.users), len(data.teams)) + b.Logf("Largest root folder: %s with %d descendants", data.largestRootFolder, data.largestRootDescCount) + + return srv, data +} + +// BenchmarkCheck measures the performance of Check requests +func BenchmarkCheck(b *testing.B) { + srv, data := setupBenchmarkServer(b) + ctx := newContextWithNamespace() + + // Helper to create check requests + newCheckReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest { + return &authzv1.CheckRequest{ + Namespace: benchNamespace, + Subject: subject, + Verb: verb, + Group: group, + Resource: resource, + Folder: folder, + Name: name, + } + } + + usersPerPattern := len(data.users) / 7 + + b.Run("GroupResourceDirect", func(b *testing.B) { + // User with group_resource permission - should have access to everything + user := data.users[0] // First user has GroupResource permission + resource := data.resources[rand.Intn(len(data.resources))] + folder := data.resourceFolders[resource] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + if !res.GetAllowed() { + b.Fatal("expected access to be allowed") + } + } + }) + + // Test folder inheritance at each depth level (0 to maxDepth) + // User has permission on ROOT folder (depth 0), we check access at each deeper level + rootUser := "user:depth-0-access" // has view permission on root folder + for depth := 0; depth <= data.maxDepth; depth++ { + depth := depth // capture for closure + if len(data.foldersByDepth[depth]) == 0 { + continue + } + b.Run(fmt.Sprintf("FolderInheritance/Depth%d", depth), func(b *testing.B) { + resource := data.resources[0] + folder := data.foldersByDepth[depth][0] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(rootUser, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + _ = res.GetAllowed() + } + }) + } + + b.Run("FolderResourceScoped", func(b *testing.B) { + // User with folder-scoped resource permission + user := data.users[3*usersPerPattern] + folderIdx := 0 + folder := data.folders[folderIdx] + resource := data.resources[folderIdx] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + _ = res.GetAllowed() + } + }) + + b.Run("DirectResource", func(b *testing.B) { + // User with direct resource permission + user := data.users[4*usersPerPattern] + resourceIdx := 0 + resource := data.resources[resourceIdx] + folder := data.resourceFolders[resource] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + _ = res.GetAllowed() + } + }) + + b.Run("TeamMembership", func(b *testing.B) { + // User who is a team member, team has folder permission + user := data.users[5*usersPerPattern] + teamIdx := 0 + folderIdx := teamIdx % len(data.folders) + folder := data.folders[folderIdx] + resource := data.resources[folderIdx%len(data.resources)] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + _ = res.GetAllowed() + } + }) + + b.Run("NoAccess", func(b *testing.B) { + // User with no permissions - tests denial path + user := data.users[len(data.users)-1] // Last user has no permissions + resource := data.resources[0] + folder := data.resourceFolders[resource] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource)) + if err != nil { + b.Fatal(err) + } + if res.GetAllowed() { + b.Fatal("expected access to be denied") + } + } + }) + + b.Run("FolderCheck", func(b *testing.B) { + // Direct folder access check + user := data.users[usersPerPattern] + folder := data.rootFolder + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource, "", folder)) + if err != nil { + b.Fatal(err) + } + _ = res.GetAllowed() + } + }) +} + +func BenchmarkBatchCheck(b *testing.B) { + srv, data := setupBenchmarkServer(b) + ctx := newContextWithNamespace() + + // Helper to create batch check requests + newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { + return &authzextv1.BatchCheckRequest{ + Namespace: benchNamespace, + Subject: subject, + Items: items, + } + } + + // Helper to create batch items for resources in folders + createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem { + items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + for i := 0; i < batchCheckSize && i < len(resources); i++ { + resource := resources[i] + items = append(items, &authzextv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: resource, + Folder: resourceFolders[resource], + }) + } + return items + } + + // Helper to create batch items for folders at a specific depth + createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem { + items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + for _, folder := range folders { + if folderDepths[folder] == depth && len(items) < batchCheckSize { + items = append(items, &authzextv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-in-%s", folder), + Folder: folder, + }) + } + } + // Fill remaining slots if needed + for len(items) < batchCheckSize && len(folders) > 0 { + folder := folders[len(items)%len(folders)] + items = append(items, &authzextv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-%d", len(items)), + Folder: folder, + }) + } + return items + } + + usersPerPattern := len(data.users) / numPermissionPatterns + + b.Run("GroupResourceDirect", func(b *testing.B) { + // User with group_resource permission - should have access to everything + user := data.users[0] + items := createBatchItems(data.resources, data.resourceFolders) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("FolderInheritance/Depth1", func(b *testing.B) { + // User with folder permission on shallow folder + user := data.users[usersPerPattern] + items := createFolderBatchItems(data.folders, 1, data.folderDepths) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("FolderInheritance/Depth4", func(b *testing.B) { + // User with folder permission on mid-depth folder + user := data.users[2*usersPerPattern] + items := createFolderBatchItems(data.folders, 4, data.folderDepths) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("FolderInheritance/Depth7", func(b *testing.B) { + // Check access on deepest folders (worst case for inheritance traversal) + user := data.users[usersPerPattern] + items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("DirectResource", func(b *testing.B) { + // User with direct resource permission + user := data.users[4*usersPerPattern] + items := createBatchItems(data.resources, data.resourceFolders) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("TeamMembership", func(b *testing.B) { + // User who is a team member, team has folder permission + user := data.users[5*usersPerPattern] + items := createBatchItems(data.resources, data.resourceFolders) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("NoAccess", func(b *testing.B) { + // User with no permissions - tests denial path + user := data.users[len(data.users)-1] + items := createBatchItems(data.resources, data.resourceFolders) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) + + b.Run("MixedFolders", func(b *testing.B) { + // Batch of items across different folder depths + user := data.users[usersPerPattern] + items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + for i := 0; i < batchCheckSize; i++ { + folder := data.folders[i%len(data.folders)] + items = append(items, &authzextv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-%d", i), + Folder: folder, + }) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Groups + } + }) +} + +// BenchmarkList measures the performance of List requests (Compile equivalent) +func BenchmarkList(b *testing.B) { + srv, data := setupBenchmarkServer(b) + baseCtx := newContextWithNamespace() + + // Helper to create list requests + newListReq := func(subject, verb, group, resource string) *authzv1.ListRequest { + return &authzv1.ListRequest{ + Namespace: benchNamespace, + Subject: subject, + Verb: verb, + Group: group, + Resource: resource, + } + } + + // Helper to create context with timeout + ctxWithTimeout := func() (context.Context, context.CancelFunc) { + return context.WithTimeout(baseCtx, listTimeout) + } + + usersPerPattern := len(data.users) / 7 + + b.Run("AllAccess", func(b *testing.B) { + // User with group_resource permission - should return All=true quickly + user := data.users[0] + b.Logf("Test: User with group_resource permission (access to ALL dashboards)") + b.Logf("Expected: All=true returned immediately without ListObjects call") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource)) + cancel() + if err != nil { + b.Fatalf("Error: %v", err) + } + if !res.GetAll() { + b.Fatal("expected All=true for user with group_resource permission") + } + } + }) + + b.Run("FolderScoped", func(b *testing.B) { + // User with folder permissions - should return folder list + user := data.users[usersPerPattern] + b.Logf("Test: User with direct folder permission on a single folder") + b.Logf("Expected: Returns list of folders user has access to") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource)) + cancel() + if err != nil { + b.Fatalf("Error: %v", err) + } + if i == 0 { + b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll()) + } + } + }) + + b.Run("DirectResources", func(b *testing.B) { + // User with direct resource permissions - should return items list + user := data.users[4*usersPerPattern] + b.Logf("Test: User with direct permission on specific resources") + b.Logf("Expected: Returns list of specific resources user has access to") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource)) + cancel() + if err != nil { + b.Fatalf("Error: %v", err) + } + if i == 0 { + b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll()) + } + } + }) + + b.Run("NoAccess", func(b *testing.B) { + // User with no permissions - should return empty results + user := data.users[len(data.users)-1] + b.Logf("Test: User with NO permissions (denial case)") + b.Logf("Expected: Empty results") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource)) + cancel() + if err != nil { + b.Fatalf("Error: %v", err) + } + if i == 0 { + b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll()) + } + } + }) + + b.Run("LargeRootFolder", func(b *testing.B) { + // User with access to root folder that has many descendants + user := "user:large-root-access" + b.Logf("Test: User with permission on ROOT folder (folder-0)") + b.Logf("Root folder %s has %d total descendants", data.largestRootFolder, data.largestRootDescCount) + b.Logf("Expected: ListObjects should return folders through inheritance") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + start := time.Now() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource)) + elapsed := time.Since(start) + cancel() + if err != nil { + b.Fatalf("Error after %v: %v", elapsed, err) + } + if i == 0 { + b.Logf("Result: %d folders returned in %v (descendants: %d)", + len(res.GetItems()), elapsed, data.largestRootDescCount) + } + } + }) + + // Test List at various folder depths to find breaking point + b.Run("ByDepth", func(b *testing.B) { + b.Logf("Testing List performance at various folder depths (timeout: %v)", listTimeout) + b.Logf("Tree structure: %d folders per level, %d max depth", foldersPerLevel, data.maxDepth) + + for depth := 0; depth <= data.maxDepth; depth++ { + if len(data.foldersByDepth[depth]) == 0 { + continue + } + + folder := data.foldersByDepth[depth][0] + descendants := data.folderDescendants[folder] + user := fmt.Sprintf("user:depth-%d-access", depth) + + b.Run(fmt.Sprintf("Depth%d_%dDescendants", depth, descendants), func(b *testing.B) { + b.Logf("Test: User with permission on folder at depth %d", depth) + b.Logf("Folder: %s, Descendants: %d", folder, descendants) + + // First, do a single timed run to report + ctx, cancel := ctxWithTimeout() + start := time.Now() + res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource)) + elapsed := time.Since(start) + cancel() + + if err != nil { + b.Logf("FAILED after %v: %v", elapsed, err) + if elapsed >= listTimeout { + b.Logf("TIMEOUT: List took longer than %v", listTimeout) + } + b.Skip("Skipping benchmark iterations due to error") + return + } + + b.Logf("Result: %d folders in %v", len(res.GetItems()), elapsed) + + if elapsed > 5*time.Second { + b.Logf("WARNING: Single List took %v, skipping benchmark iterations", elapsed) + b.Skip("Too slow for benchmark iterations") + return + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx, cancel := ctxWithTimeout() + _, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource)) + cancel() + if err != nil { + b.Fatalf("Error: %v", err) + } + } + }) + } + }) +} diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 916c84e5c0a..2f49641f17f 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -126,8 +126,14 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou return &authzv1.CheckResponse{Allowed: false}, nil } + // Use optimized folder permission relations for permission management + checkRelation := relation + if resource.Type() == common.TypeFolder { + checkRelation = common.FolderPermissionRelation(relation) + } + // Check if subject has direct access to resource - res, err := s.openfgaCheck(ctx, store, subject, relation, resourceIdent, contextuals, nil) + res, err := s.openfgaCheck(ctx, store, subject, checkRelation, resourceIdent, contextuals, nil) if err != nil { return nil, err } @@ -143,14 +149,15 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res defer span.End() var ( - folderIdent = resource.FolderIdent() - resourceCtx = resource.Context() - folderRelation = common.SubresourceRelation(relation) + folderIdent = resource.FolderIdent() + resourceCtx = resource.Context() + folderRelation = common.SubresourceRelation(relation) + folderCheckRelation = common.FolderPermissionRelation(relation) ) if folderIdent != "" && isFolderPermissionBasedResource(resource.GroupResource()) { // Check if resource inherits permissions from the folder (like dashboards in a folder) - res, err := s.openfgaCheck(ctx, store, subject, relation, folderIdent, contextuals, resourceCtx) + res, err := s.openfgaCheck(ctx, store, subject, folderCheckRelation, folderIdent, contextuals, resourceCtx) if err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 59a192fe6a0..d8e8fa01526 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -212,4 +212,16 @@ func testCheck(t *testing.T, server *Server) { require.NoError(t, err) assert.True(t, res.GetAllowed(), "user should be able to view dashboards in folder 6") }) + + t.Run("user:18 should be able to create folder in root folder", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:18", utils.VerbCreate, folderGroup, folderResource, "", "", "")) + require.NoError(t, err) + assert.Equal(t, true, res.GetAllowed()) + }) + + t.Run("user:18 should be able to create dashboard in root folder", func(t *testing.T) { + res, err := server.Check(newContextWithNamespace(), newReq("user:18", utils.VerbCreate, dashboardGroup, dashboardResource, "", "", "")) + require.NoError(t, err) + assert.Equal(t, true, res.GetAllowed()) + }) } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 216e8df933e..9734f186d2a 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -85,6 +85,12 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour resourceCtx = resource.Context() ) + // Use optimized folder permission relations for permission management + listRelation := relation + if resource.Type() == common.TypeFolder { + listRelation = common.FolderPermissionRelation(relation) + } + var items []string if resource.HasSubresource() && common.IsSubresourceRelation(subresourceRelation) { // List requested subresources @@ -110,7 +116,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour StoreId: store.ID, AuthorizationModelId: store.ModelID, Type: resource.Type(), - Relation: relation, + Relation: listRelation, User: subject, ContextualTuples: contextuals, }) @@ -129,8 +135,9 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso defer span.End() var ( - folderRelation = common.SubresourceRelation(relation) - resourceCtx = resource.Context() + folderRelation = common.SubresourceRelation(relation) + folderListRelation = common.FolderPermissionRelation(relation) // Optimized for permission management + resourceCtx = resource.Context() ) // 1. List all folders subject has access to resource type in @@ -159,7 +166,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso StoreId: store.ID, AuthorizationModelId: store.ModelID, Type: common.TypeFolder, - Relation: relation, + Relation: folderListRelation, User: subject, Context: resourceCtx, ContextualTuples: contextuals, diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index 15a57404bbc..3af0d06180d 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -8,6 +8,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/codes" + "google.golang.org/grpc/status" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -35,6 +36,9 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) + if _, ok := status.FromError(err); ok { + return nil, err + } s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform mutate request") } @@ -43,7 +47,7 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au } func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { - if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + if err := authorizeWrite(ctx, req.GetNamespace(), s.cfg); err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder_test.go b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go index a01c3b08740..410e70099d8 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_folder_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go @@ -30,7 +30,7 @@ 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{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -61,7 +61,7 @@ func testMutateFolders(t *testing.T, srv *Server) { }) t.Run("should delete folder parent relation", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -88,7 +88,7 @@ func testMutateFolders(t *testing.T, srv *Server) { }) t.Run("should clean up all parent relations", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -115,7 +115,7 @@ func testMutateFolders(t *testing.T, srv *Server) { }) t.Run("should perform batch mutate if multiple operations are provided", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { 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 index 7c7472cf6ac..899ba4be86f 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_org_role_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_org_role_test.go @@ -25,7 +25,7 @@ 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{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -63,7 +63,7 @@ func testMutateOrgRoles(t *testing.T, srv *Server) { }) t.Run("should add user org role and delete old role", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go index 3336d9d813b..296ed9e2be3 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go @@ -28,7 +28,7 @@ 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{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -76,7 +76,7 @@ func testMutateResourcePermissions(t *testing.T, srv *Server) { require.NoError(t, err) require.Len(t, res.Tuples, 2) - _, err = srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err = srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go index 87db285c360..3d4b0c5db51 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go @@ -25,7 +25,7 @@ func testMutateRoleBindings(t *testing.T, srv *Server) { setupMutateRoleBindings(t, srv) t.Run("should update user role and delete old role", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -75,7 +75,7 @@ func testMutateRoleBindings(t *testing.T, srv *Server) { }) t.Run("should assign role to basic role", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { diff --git a/pkg/services/authz/zanzana/server/server_mutate_roles_test.go b/pkg/services/authz/zanzana/server/server_mutate_roles_test.go index 1128c0c0f72..e6c92dc66f8 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_roles_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_roles_test.go @@ -25,7 +25,7 @@ func testMutateRoles(t *testing.T, srv *Server) { setupMutateRoles(t, srv) t.Run("should update role and delete old role permissions", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { diff --git a/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go index 5103b142fc5..a4c8d73d856 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go @@ -25,7 +25,7 @@ func testMutateTeamBindings(t *testing.T, srv *Server) { setupMutateTeamBindings(t, srv) t.Run("should update user team binding and delete old team binding", func(t *testing.T) { - _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { diff --git a/pkg/services/authz/zanzana/server/server_mutate_test.go b/pkg/services/authz/zanzana/server/server_mutate_test.go index c1fcfabbe43..57949b9ef12 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_test.go @@ -5,6 +5,8 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" @@ -33,7 +35,7 @@ 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{ + _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ Namespace: "default", Operations: []*v1.MutateOperation{ { @@ -133,6 +135,25 @@ func testMutate(t *testing.T, srv *Server) { require.NoError(t, err) require.Len(t, res.Tuples, 0) }) + + t.Run("should reject mutate without zanzana:update", 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.Error(t, err) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + }) } func TestDeduplicateTupleKeys(t *testing.T) { diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 63cf8ee2a50..b910d1f492a 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -71,6 +72,8 @@ func setup(t *testing.T, srv *Server) *Server { common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), common.NewFolderTuple("user:17", common.RelationSetView, "4"), + common.NewFolderTuple("user:18", common.RelationCreate, "general"), + common.NewFolderResourceTuple("user:18", common.RelationCreate, dashboardGroup, dashboardResource, "", "general"), } return setupOpenFGADatabase(t, srv, tuples) @@ -216,11 +219,21 @@ func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKe } func newContextWithNamespace() context.Context { + return newContextWithNamespaceAndPermissions() +} + +func newContextWithNamespaceAndPermissions(perms ...string) context.Context { ctx := context.Background() ctx = claims.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ Rest: authnlib.AccessTokenClaims{ - Namespace: "*", + Namespace: "*", + Permissions: perms, + DelegatedPermissions: perms, }, })) return ctx } + +func newContextWithZanzanaUpdatePermission() context.Context { + return newContextWithNamespaceAndPermissions(zanzana.TokenPermissionUpdate) +} diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index d16d706a320..dd0e4b11f0d 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -8,6 +8,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/codes" + "google.golang.org/grpc/status" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -25,6 +26,9 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) + if _, ok := status.FromError(err); ok { + return nil, err + } s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform write request") } @@ -33,7 +37,7 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth } func (s *Server) write(ctx context.Context, req *authzextv1.WriteRequest) (*authzextv1.WriteResponse, error) { - if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + if err := authorizeWrite(ctx, req.GetNamespace(), s.cfg); err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_write_test.go b/pkg/services/authz/zanzana/server/server_write_test.go new file mode 100644 index 00000000000..892edcb694c --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_write_test.go @@ -0,0 +1,46 @@ +package server + +import ( + "testing" + + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" + "github.com/stretchr/testify/require" +) + +func TestWriteAuthorization(t *testing.T) { + cfg := setting.NewCfg() + testStore := sqlstore.NewTestStore(t, sqlstore.WithCfg(cfg)) + srv := setupOpenFGAServer(t, testStore, cfg) + setup(t, srv) + + req := &authzextv1.WriteRequest{ + Namespace: namespace, + Writes: &authzextv1.WriteRequestWrites{ + TupleKeys: []*authzextv1.TupleKey{ + { + // Folder parent tuples are valid without any relationship condition. + User: "folder:1", + Relation: common.RelationParent, + Object: "folder:write-authz-test", + }, + }, + }, + } + + t.Run("denies Write without zanzana:update", func(t *testing.T) { + _, err := srv.Write(newContextWithNamespace(), req) + require.Error(t, err) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + }) + + t.Run("allows Write with zanzana:update", func(t *testing.T) { + _, err := srv.Write(newContextWithZanzanaUpdatePermission(), req) + require.NoError(t, err) + }) +} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 6cadfc0c199..261ea78830d 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -16,6 +16,9 @@ const ( TypeNamespace = common.TypeGroupResouce ) +// TokenPermissionUpdate is required for callers to perform write operations against Zanzana (Mutate/Write). +const TokenPermissionUpdate = "zanzana:update" //nolint:gosec // G101: permission identifier, not a credential. + const ( RelationTeamMember = common.RelationTeamMember RelationTeamAdmin = common.RelationTeamAdmin diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 0334cfc8990..fd30728aa35 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -44,6 +44,11 @@ type DashboardService interface { GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) } +type DashboardAccessService interface { + // The user as access to {VERB} the requested dashboard + HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error) +} + type PermissionsRegistrationService interface { RegisterDashboardPermissions(service accesscontrol.DashboardPermissionsService) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index d20a9525622..f5ba0e096dc 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -5,9 +5,10 @@ package dashboards import ( context "context" - identity "github.com/grafana/grafana/pkg/apimachinery/identity" mock "github.com/stretchr/testify/mock" + identity "github.com/grafana/grafana/pkg/apimachinery/identity" + model "github.com/grafana/grafana/pkg/services/search/model" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -529,6 +530,11 @@ func (_m *FakeDashboardService) ValidateDashboardRefreshInterval(minRefreshInter return r0 } +// CanViewDashboard uses the access control service to check if the requested user can see a dashboard +func (_m *FakeDashboardService) HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error) { + return true, nil +} + // NewFakeDashboardService creates a new instance of FakeDashboardService. 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 NewFakeDashboardService(t interface { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 3c661ee6b9c..c68263db693 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -304,8 +304,15 @@ type DeleteDashboardCommand struct { RemovePermissions bool } +type ProvisioningConfig struct { + Name string + OrgID int64 + Folder string + AllowUIUpdates bool +} + type DeleteOrphanedProvisionedDashboardsCommand struct { - ReaderNames []string + Config []ProvisioningConfig } type DashboardProvisioningSearchResults struct { @@ -405,6 +412,8 @@ type DashboardSearchProjection struct { FolderTitle string SortMeta int64 Tags []string + ManagedBy utils.ManagerKind + ManagerId string Deleted *time.Time } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index b63b4a40f96..e105aaa3325 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -67,6 +67,7 @@ var ( _ dashboards.DashboardService = (*DashboardServiceImpl)(nil) _ dashboards.DashboardProvisioningService = (*DashboardServiceImpl)(nil) _ dashboards.PluginService = (*DashboardServiceImpl)(nil) + _ dashboards.DashboardAccessService = (*DashboardServiceImpl)(nil) daysInTrash = 24 * 30 * time.Hour tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service") @@ -100,6 +101,38 @@ type DashboardServiceImpl struct { dashboardPermissionsReady chan struct{} } +// CanViewDashboard uses the access control service to check if the requested user can see a dashboard +func (dr *DashboardServiceImpl) HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error) { + ns, err := claims.ParseNamespace(namespace) + if err != nil { + return false, err + } + dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{ + UID: name, + OrgID: ns.OrgID, + }) + if err != nil || dash == nil { + return false, nil + } + var action string + switch verb { + case utils.VerbGet: + action = dashboards.ActionDashboardsRead + case utils.VerbUpdate: + action = dashboards.ActionDashboardsWrite + default: + return false, fmt.Errorf("unsupported verb") + } + + evaluator := accesscontrol.EvalPermission(action, + dashboards.ScopeDashboardsProvider.GetResourceScopeUID(name)) + canView, err := dr.ac.Evaluate(ctx, user, evaluator) + if err != nil || !canView { + return false, nil + } + return true, nil +} + func (dr *DashboardServiceImpl) startK8sDeletedDashboardsCleanupJob(ctx context.Context) chan struct{} { done := make(chan struct{}) go func() { @@ -877,24 +910,32 @@ 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 { return err } + orgIDs := make([]int64, 0, len(orgs)) + for _, org := range orgs { + orgIDs = append(orgIDs, org.ID) + } + + if err := dr.DeleteDuplicateProvisionedDashboards(ctx, orgIDs, cmd.Config); err != nil { + dr.log.Error("Failed to delete duplicate provisioned dashboards", "error", err) + } + + currentNames := make([]string, 0, len(cmd.Config)) + for _, cfg := range cmd.Config { + currentNames = append(currentNames, cfg.Name) + } + for _, org := range orgs { ctx, _ := identity.WithServiceIdentity(ctx, org.ID) // find all dashboards in the org that have a file repo set that is not in the given readers list foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck - ManagerIdentityNotIn: cmd.ReaderNames, + ManagerIdentityNotIn: currentNames, OrgId: org.ID, }) if err != nil { @@ -921,7 +962,129 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. return nil } -func (dr *DashboardServiceImpl) DeleteDuplicateProvisionedDashboards(ctx context.Context) error { +// searchExistingProvisionedData fetches provisioned data for the purposes of +// duplication cleanup. Returns the set of folder UIDs for folders with the +// given title, and the set of resources contained in those folders. +func (dr *DashboardServiceImpl) searchExistingProvisionedData( + ctx context.Context, orgID int64, folderTitle string, +) ([]string, []dashboards.DashboardSearchProjection, error) { + ctx, user := identity.WithServiceIdentity(ctx, orgID) + cmd := folder.SearchFoldersQuery{ + OrgID: orgID, + SignedInUser: user, + Title: folderTitle, + TitleExactMatch: true, + } + + searchResults, err := dr.folderService.SearchFolders(ctx, cmd) + if err != nil { + return nil, nil, fmt.Errorf("checking if provisioning reset is required: %w", err) + } + + var matchingFolders []string //nolint:prealloc + for _, result := range searchResults { + f, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{ + OrgID: orgID, + UID: &result.UID, + SignedInUser: user, + }) + if err != nil { + return nil, nil, err + } + + // We are only interested in folders at the top-level of the folder hierarchy. + // Cleanup is not performed for provisioned folders that were moved to + // a different location. + if f.ParentUID != "" { + continue + } + + matchingFolders = append(matchingFolders, f.UID) + } + + if len(matchingFolders) == 0 { + // If there are no folders with the same title as the provisioned folder we + // are looking for, there is nothing to be cleaned up. + return nil, nil, nil + } + + resources, err := dr.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: orgID, + SignedInUser: user, + FolderUIDs: matchingFolders, + }) + if err != nil { + return nil, nil, err + } + + return matchingFolders, resources, nil +} + +// maybeResetProvisioning will check for duplicated provisioned dashboards in the database. These duplications +// happen when multiple provisioned dashboards of the same title are found, or multiple provisioned +// folders are found. In this case, provisioned resources are deleted, allowing the provisioning +// process to start from scratch after this function returns. +func (dr *DashboardServiceImpl) maybeResetProvisioning(ctx context.Context, orgs []int64, configs []dashboards.ProvisioningConfig) { + if skipReason := canBeAutomaticallyCleanedUp(configs); skipReason != "" { + dr.log.Info("not eligible for automated cleanup", "reason", skipReason) + return + } + + folderTitle := configs[0].Folder + provisionedNames := map[string]bool{} + for _, c := range configs { + provisionedNames[c.Name] = true + } + + for _, orgID := range orgs { + ctx, user := identity.WithServiceIdentity(ctx, orgID) + provFolders, resources, err := dr.searchExistingProvisionedData(ctx, orgID, folderTitle) + if err != nil { + dr.log.Error("failed to search for provisioned data for cleanup", "org", orgID, "error", err) + continue + } + + steps, err := cleanupSteps(provFolders, resources, provisionedNames) + if err != nil { + dr.log.Warn("not possible to perform automated duplicate cleanup", "org", orgID, "error", err) + continue + } + + for _, step := range steps { + var err error + + switch step.Type { + case searchstore.TypeDashboard: + err = dr.deleteDashboard(ctx, 0, step.UID, orgID, false) + case searchstore.TypeFolder: + err = dr.folderService.Delete(ctx, &folder.DeleteFolderCommand{ + OrgID: orgID, + SignedInUser: user, + UID: step.UID, + }) + } + + if err == nil { + dr.log.Info("deleted duplicated provisioned resource", + "type", step.Type, "uid", step.UID, + ) + } else { + dr.log.Error("failed to delete duplicated provisioned resource", + "type", step.Type, "uid", step.UID, "error", err, + ) + } + } + } +} + +func (dr *DashboardServiceImpl) DeleteDuplicateProvisionedDashboards(ctx context.Context, orgs []int64, configs []dashboards.ProvisioningConfig) error { + // Start from scratch if duplications that cannot be fixed by the logic + // below are found in the database. + dr.maybeResetProvisioning(ctx, orgs, configs) + + // cleanup duplicate provisioned dashboards (i.e., with the same name and external_id). + // Note: only works in modes 1-3. This logic can be removed once mode5 is + // enabled everywhere. duplicates, err := dr.dashboardStore.GetDuplicateProvisionedDashboards(ctx) if err != nil { return err @@ -1305,6 +1468,11 @@ func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query * if err != nil { return nil, err } + + if query.ID <= 0 { + return nil, dashboards.ErrDashboardNotFound + } + result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ OrgId: requester.GetOrgID(), DashboardIds: []int64{query.ID}, @@ -1316,7 +1484,7 @@ func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query * if len(result) == 0 { return nil, dashboards.ErrDashboardNotFound } else if len(result) > 1 { - return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result)) + return nil, fmt.Errorf("unexpected number of dashboards for id %d. found: %d. desired: 1", query.ID, len(result)) } return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug, FolderUID: result[0].FolderUID}, nil @@ -1506,6 +1674,8 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb FolderTitle: folderTitle, FolderID: folderID, FolderSlug: slugify.Slugify(folderTitle), + ManagedBy: hit.ManagedBy.Kind, + ManagerId: hit.ManagedBy.ID, Tags: hit.Tags, } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 35ac47afd9a..a3745f864c2 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -779,7 +779,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { }, nil).Twice() err := service.DeleteOrphanedProvisionedDashboards(context.Background(), &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ - ReaderNames: []string{"test"}, + Config: []dashboards.ProvisioningConfig{{Name: "test"}}, }) require.NoError(t, err) k8sCliMock.AssertExpectations(t) @@ -874,7 +874,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { }, nil).Once() err := singleOrgService.DeleteOrphanedProvisionedDashboards(ctx, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ - ReaderNames: []string{"test"}, + Config: []dashboards.ProvisioningConfig{{Name: "test"}}, }) require.NoError(t, err) k8sCliMock.AssertExpectations(t) @@ -906,7 +906,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { }, nil) err := singleOrgService.DeleteOrphanedProvisionedDashboards(ctx, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ - ReaderNames: []string{"test"}, + Config: []dashboards.ProvisioningConfig{{Name: "test"}}, }) require.NoError(t, err) k8sCliMock.AssertExpectations(t) @@ -1663,6 +1663,13 @@ func TestGetDashboardUIDByID(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedResult, result) k8sCliMock.AssertExpectations(t) + + // 0 should return error + _, err = service.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{ + ID: 0, + }) + require.Error(t, err) + require.Equal(t, dashboards.ErrDashboardNotFound, err) } func TestUnstructuredToLegacyDashboard(t *testing.T) { diff --git a/pkg/services/dashboards/service/provisioning_cleanup.go b/pkg/services/dashboards/service/provisioning_cleanup.go new file mode 100644 index 00000000000..ca5fe75921a --- /dev/null +++ b/pkg/services/dashboards/service/provisioning_cleanup.go @@ -0,0 +1,107 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" +) + +// canBeAutomaticallyCleanedUp determines whether this instance can be automatically cleaned up +// if duplicated provisioned resources are found. To ensure the process does not delete +// resources it shouldn't, automatic cleanups only happen if all provisioned dashboards +// are stored in the same folder (by title), and no dashboards allow UI updates. +func canBeAutomaticallyCleanedUp(configs []dashboards.ProvisioningConfig) string { + if len(configs) == 0 { + return "no provisioned dashboards" + } + + folderTitle := configs[0].Folder + if len(folderTitle) == 0 { + return fmt.Sprintf("dashboard has no folder: %s", configs[0].Name) + } + + for _, cfg := range configs { + if cfg.AllowUIUpdates { + return "contains dashboards with allowUiUpdates" + } + + if cfg.Folder != folderTitle { + return "dashboards provisioned across multiple folders" + } + } + + return "" +} + +type deleteProvisionedResource struct { + Type string + UID string +} + +// cleanupSteps computes the sequence of steps to be performed in order to cleanup the +// provisioning resources and allow the process to start from scratch when duplication +// is detected. The sequence of steps will dictate the order in which dashboards and folders +// are to be deleted. +func cleanupSteps(provFolders []string, resources []dashboards.DashboardSearchProjection, configDashboards map[string]bool) ([]deleteProvisionedResource, error) { + var hasDuplicatedProvisionedDashboard bool + var hasUserCreatedResource bool + var uniqueNames = map[string]struct{}{} + var deleteProvisionedDashboards []deleteProvisionedResource //nolint:prealloc + + for _, r := range resources { + // nolint:staticcheck + if r.IsFolder || r.ManagedBy != utils.ManagerKindClassicFP { + hasUserCreatedResource = true + continue + } + + // Only delete dashboards if they are included in the provisioning configuration + // for this instance. + if !configDashboards[r.ManagerId] { + continue + } + + if _, exists := uniqueNames[r.ManagerId]; exists { + hasDuplicatedProvisionedDashboard = true + } + + uniqueNames[r.ManagerId] = struct{}{} + deleteProvisionedDashboards = append(deleteProvisionedDashboards, deleteProvisionedResource{ + Type: searchstore.TypeDashboard, + UID: r.UID, + }) + } + + if len(provFolders) == 0 { + // When there are no provisioned folders, there is nothing to do. + return nil, nil + } else if len(provFolders) == 1 { + // If only one folder was found, keep it and delete the provisioned dashboards if + // duplication was found. + if hasDuplicatedProvisionedDashboard { + return deleteProvisionedDashboards, nil + } + } else { + // If multiple folders were found *and* a user-created resource exists in + // one of them, bail, as we wouldn't be able to delete one of the duplicated folders. + if hasUserCreatedResource { + return nil, errors.New("multiple provisioning folders exist with at least one user-created resource") + } + + // Delete provisioned dashboards first, and then the folders. + steps := deleteProvisionedDashboards + for _, uid := range provFolders { + steps = append(steps, deleteProvisionedResource{ + Type: searchstore.TypeFolder, + UID: uid, + }) + } + + return steps, nil + } + + return nil, nil +} diff --git a/pkg/services/dashboards/service/provisioning_cleanup_test.go b/pkg/services/dashboards/service/provisioning_cleanup_test.go new file mode 100644 index 00000000000..049dc420f2d --- /dev/null +++ b/pkg/services/dashboards/service/provisioning_cleanup_test.go @@ -0,0 +1,279 @@ +package service + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/stretchr/testify/require" +) + +func Test_canBeAutomaticallyCleanedUp(t *testing.T) { + testCases := []struct { + name string + configs []dashboards.ProvisioningConfig + expectedSkip string + }{ + { + name: "no dashboards defined in the configuration", + configs: []dashboards.ProvisioningConfig{}, + expectedSkip: "no provisioned dashboards", + }, + { + name: "first defined dashboard has no folder defined", + configs: []dashboards.ProvisioningConfig{ + {Name: "1", Folder: ""}, + {Folder: "f1"}, + }, + expectedSkip: "dashboard has no folder: 1", + }, + { + name: "one of the provisioned dashboards has no folder defined", + configs: []dashboards.ProvisioningConfig{ + {Name: "1", Folder: "f1"}, + {Name: "2", Folder: "f1"}, + {Name: "3", Folder: ""}, + {Name: "4", Folder: "f1"}, + }, + expectedSkip: "dashboards provisioned across multiple folders", + }, + { + name: "one of the provisioned dashboards allows UI updates", + configs: []dashboards.ProvisioningConfig{ + {Name: "1", Folder: "f1"}, + {Name: "2", Folder: "f1", AllowUIUpdates: true}, + {Name: "3", Folder: "f1"}, + {Name: "4", Folder: "f1"}, + }, + expectedSkip: "contains dashboards with allowUiUpdates", + }, + { + name: "one of the provisioned dashboards is in a different folder", + configs: []dashboards.ProvisioningConfig{ + {Name: "1", Folder: "f1"}, + {Name: "2", Folder: "f1"}, + {Name: "3", Folder: "f1"}, + {Name: "4", Folder: "different"}, + }, + expectedSkip: "dashboards provisioned across multiple folders", + }, + { + name: "can be skipped when all conditions are met", + configs: []dashboards.ProvisioningConfig{ + {Name: "1", Folder: "f1"}, + {Name: "2", Folder: "f1"}, + {Name: "3", Folder: "f1"}, + {Name: "4", Folder: "f1"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectedSkip, canBeAutomaticallyCleanedUp(tc.configs)) + }) + } +} + +func Test_cleanupSteps(t *testing.T) { + isDashboard, isFolder := false, true + + fromUser := func(uid, name string, isFolder bool) dashboards.DashboardSearchProjection { + return dashboards.DashboardSearchProjection{ + UID: uid, + ManagerId: name, + IsFolder: isFolder, + } + } + + provisioned := func(uid, name string, isFolder bool) dashboards.DashboardSearchProjection { + dashboard := fromUser(uid, name, isFolder) + dashboard.ManagedBy = utils.ManagerKindClassicFP //nolint:staticcheck + return dashboard + } + + testCases := []struct { + name string + provisionedFolders []string + provisionedResources []dashboards.DashboardSearchProjection + configDashboards []string + expectedSteps []deleteProvisionedResource + expectedErr string + }{ + { + name: "no provisioned folders, nothing to do", + provisionedFolders: []string{}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + }, + }, + { + name: "multiple folders, a user-created dashboard in one of them", + provisionedFolders: []string{"folder1", "folder2"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + fromUser("d3", "User1", isDashboard), + provisioned("d4", "Provisioned3", isDashboard), + }, + expectedErr: "multiple provisioning folders exist with at least one user-created resource", + }, + { + name: "multiple folders, a user-created folder in one of them", + provisionedFolders: []string{"folder1", "folder2"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned3", isDashboard), + fromUser("f1", "UserFolder1", isFolder), + }, + expectedErr: "multiple provisioning folders exist with at least one user-created resource", + }, + { + name: "single folder, some dashboards duplicated", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + // Provisioned1 is duplicated. + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned1", isDashboard), + provisioned("d4", "Provisioned3", isDashboard), + }, + expectedSteps: []deleteProvisionedResource{ + {Type: searchstore.TypeDashboard, UID: "d1"}, + {Type: searchstore.TypeDashboard, UID: "d2"}, + {Type: searchstore.TypeDashboard, UID: "d3"}, + {Type: searchstore.TypeDashboard, UID: "d4"}, + }, + }, + { + name: "single folder, duplicated dashboards, user-created dashboards are ignored", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + // Provisioned1 is duplicated. + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + fromUser("d3", "User1", isDashboard), + provisioned("d4", "Provisioned3", isDashboard), + provisioned("d5", "Provisioned1", isDashboard), + }, + // User dashboard (d3) is not deleted. + expectedSteps: []deleteProvisionedResource{ + {Type: searchstore.TypeDashboard, UID: "d1"}, + {Type: searchstore.TypeDashboard, UID: "d2"}, + {Type: searchstore.TypeDashboard, UID: "d4"}, + {Type: searchstore.TypeDashboard, UID: "d5"}, + }, + }, + { + name: "single folder, duplicated dashboards, user-created folders are ignored", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + // Provisioned1 is duplicated. + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned3", isDashboard), + provisioned("d4", "Provisioned1", isDashboard), + fromUser("f1", "UserFolder1", isFolder), + }, + // User folder (f1) is not deleted. + expectedSteps: []deleteProvisionedResource{ + {Type: searchstore.TypeDashboard, UID: "d1"}, + {Type: searchstore.TypeDashboard, UID: "d2"}, + {Type: searchstore.TypeDashboard, UID: "d3"}, + {Type: searchstore.TypeDashboard, UID: "d4"}, + }, + }, + { + name: "multiple folders, only provisioned dashboards", + provisionedFolders: []string{"folder1", "folder2"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned3", isDashboard), + provisioned("d4", "Provisioned4", isDashboard), + }, + // Delete all dashboards, then all folders. + expectedSteps: []deleteProvisionedResource{ + {Type: searchstore.TypeDashboard, UID: "d1"}, + {Type: searchstore.TypeDashboard, UID: "d2"}, + {Type: searchstore.TypeDashboard, UID: "d3"}, + {Type: searchstore.TypeDashboard, UID: "d4"}, + {Type: searchstore.TypeFolder, UID: "folder1"}, + {Type: searchstore.TypeFolder, UID: "folder2"}, + }, + }, + { + name: "single folder, only deletes dashboards defined in the config file", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned1", isDashboard), + provisioned("d4", "Provisioned4", isDashboard), + provisioned("d5", "Provisioned4", isDashboard), + }, + // Delete duplicated dashboards, but keep Provisioned4, since it's not in the config file. + expectedSteps: []deleteProvisionedResource{ + {Type: searchstore.TypeDashboard, UID: "d1"}, + {Type: searchstore.TypeDashboard, UID: "d2"}, + {Type: searchstore.TypeDashboard, UID: "d3"}, + }, + }, + { + name: "single folder, no duplicated dashboards", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + provisioned("d3", "Provisioned3", isDashboard), + provisioned("d4", "Provisioned4", isDashboard), + }, + expectedSteps: nil, // no duplicates, nothing to do + }, + { + name: "single folder, no duplicated dashboards, multiple user-created resources", + provisionedFolders: []string{"folder1"}, + configDashboards: []string{"Provisioned1", "Provisioned2", "Provisioned3", "Provisioned4"}, + provisionedResources: []dashboards.DashboardSearchProjection{ + provisioned("d1", "Provisioned1", isDashboard), + provisioned("d2", "Provisioned2", isDashboard), + fromUser("f1", "UserFolder1", isFolder), + provisioned("d3", "Provisioned3", isDashboard), + fromUser("d4", "User1", isDashboard), + provisioned("d5", "Provisioned4", isDashboard), + fromUser("d6", "User2", isDashboard), + fromUser("f2", "UserFolder2", isFolder), + }, + expectedSteps: nil, // no duplicates in the provisioned set, nothing to do + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provisionedSet := make(map[string]bool) + for _, name := range tc.configDashboards { + provisionedSet[name] = true + } + + steps, err := cleanupSteps(tc.provisionedFolders, tc.provisionedResources, provisionedSet) + if tc.expectedErr == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedSteps, steps) + } else { + require.Error(t, err) + require.Equal(t, tc.expectedErr, err.Error()) + } + }) + } +} diff --git a/pkg/services/dashboards/service/service.go b/pkg/services/dashboards/service/service.go index f526404dc0e..f56d070b695 100644 --- a/pkg/services/dashboards/service/service.go +++ b/pkg/services/dashboards/service/service.go @@ -23,3 +23,9 @@ func ProvideDashboardPluginService( ) dashboards.PluginService { return orig } + +func ProvideDashboardAccessService( + features featuremgmt.FeatureToggles, orig *DashboardServiceImpl, +) dashboards.DashboardAccessService { + return orig +} diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 2a4d31f2b4d..8f9e01bbeea 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -274,6 +274,11 @@ func (s *Service) listDashboardVersionsThroughK8s( continueToken = tempOut.GetContinue() } + // Update the continue token on the response to reflect the actual position after all fetched items. + // Without this, the response would return the token from the first fetch, causing duplicate items + // on subsequent pages when multiple fetches were needed to fill the requested limit. + out.SetContinue(continueToken) + return out, nil } diff --git a/pkg/services/dashboardversion/dashverimpl/dashver_test.go b/pkg/services/dashboardversion/dashverimpl/dashver_test.go index 909e98ee33c..18e3c295ab5 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver_test.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver_test.go @@ -268,6 +268,58 @@ func TestListDashboardVersions(t *testing.T) { }}}, res) }) + t.Run("List returns continue token when first fetch satisfies limit with more pages", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures() + + dashboardService.On("GetDashboardUIDByID", mock.Anything, + mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). + Return(&dashboards.DashboardRef{UID: "uid"}, nil) + query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 2} + mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + + firstPage := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "11", + "generation": int64(4), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{}, + }}, + {Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "generation": int64(5), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{}, + }}, + }, + } + firstMeta, err := meta.ListAccessor(firstPage) + require.NoError(t, err) + firstMeta.SetContinue("t1") // More pages exist + + mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once() + + res, err := dashboardVersionService.List(context.Background(), &query) + require.Nil(t, err) + require.Equal(t, 2, len(res.Versions)) + require.Equal(t, "t1", res.ContinueToken) // Token from first fetch when limit is satisfied + mockCli.AssertNumberOfCalls(t, "List", 1) // Only one fetch needed + }) + t.Run("List returns correct continue token across multiple pages", func(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} @@ -333,7 +385,79 @@ func TestListDashboardVersions(t *testing.T) { res, err := dashboardVersionService.List(context.Background(), &query) require.Nil(t, err) require.Equal(t, 3, len(res.Versions)) - require.Equal(t, "t1", res.ContinueToken) // Implementation returns continue token from first page + require.Equal(t, "", res.ContinueToken) // Should return token from last fetch (empty = no more pages) + mockCli.AssertNumberOfCalls(t, "List", 2) + }) + + t.Run("List returns continue token from last fetch when more pages exist", func(t *testing.T) { + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()} + mockCli := new(client.MockK8sHandler) + dashboardVersionService.k8sclient = mockCli + dashboardVersionService.features = featuremgmt.WithFeatures() + + dashboardService.On("GetDashboardUIDByID", mock.Anything, + mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")). + Return(&dashboards.DashboardRef{UID: "uid"}, nil) + query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 3} + mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) + + firstPage := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "11", + "generation": int64(4), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{}, + }}, + {Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "12", + "generation": int64(5), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{}, + }}, + }, + } + firstMeta, err := meta.ListAccessor(firstPage) + require.NoError(t, err) + firstMeta.SetContinue("t1") + + secondPage := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + "resourceVersion": "13", + "generation": int64(6), + "labels": map[string]any{ + utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck + }, + }, + "spec": map[string]any{}, + }}, + }, + } + secondMeta, err := meta.ListAccessor(secondPage) + require.NoError(t, err) + secondMeta.SetContinue("t2") // More pages exist + + mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once() + mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(secondPage, nil).Once() + + res, err := dashboardVersionService.List(context.Background(), &query) + require.Nil(t, err) + require.Equal(t, 3, len(res.Versions)) + require.Equal(t, "t2", res.ContinueToken) // Must return token from LAST fetch, not first mockCli.AssertNumberOfCalls(t, "List", 2) }) diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 304d17ee075..d59dff63c37 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -23,9 +23,7 @@ type FeatureToggles interface { // a full server restart for a change to take place. // // Deprecated: FeatureToggles.IsEnabledGlobally is deprecated and will be removed in a future release. - // Toggles that must be reliably evaluated at the service startup should be - // changed to settings (see setting.StartupSettings), and/or removed entirely. - // For app registration please use `grafana-apiserver.runtime_config` in settings.ini + // Toggles that must be reliably evaluated at the service startup should be changed to settings and/or removed entirely. IsEnabledGlobally(flag string) bool // Get the enabled flags -- this *may* also include disabled flags (with value false) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 680680791a5..3748db8e6b4 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -185,13 +185,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaDatasourcesCoreServicesSquad, }, - { - Name: "unifiedRequestLog", - Description: "Writes error logs to the request logger", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaBackendGroup, - Expression: "true", - }, { Name: "renderAuthJWT", Description: "Uses JWT-based auth for rendering instead of relying on remote cache", @@ -414,6 +407,13 @@ var ( Owner: grafanaDataProSquad, RequiresRestart: true, }, + { + Name: "kubernetesUnifiedStorageQuotas", + Description: "Adds support for Kubernetes unified storage quotas", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + RequiresRestart: true, + }, { Name: "kubernetesLogsDrilldown", Description: "Adds support for Kubernetes logs drilldown", @@ -487,13 +487,6 @@ var ( Owner: grafanaDatasourcesCoreServicesSquad, FrontendOnly: true, // and can change at startup }, - { - Name: "queryServiceFromExplore", - Description: "Routes explore requests to the new query service", - Stage: FeatureStageExperimental, - Owner: grafanaDatasourcesCoreServicesSquad, - FrontendOnly: true, - }, { Name: "cloudWatchBatchQueries", Description: "Runs CloudWatch metrics queries as separate batches", @@ -579,13 +572,6 @@ var ( FrontendOnly: false, // The restore backend feature changes behavior based on this flag Owner: grafanaDashboardsSquad, }, - { - Name: "kubernetesDashboardsV2", - Description: "Use the v2 kubernetes API in the frontend for dashboards", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaDashboardsSquad, - }, { Name: "dashboardUndoRedo", Description: "Enables undo/redo in dynamic dashboards", @@ -600,6 +586,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "drilldownRecommendations", + Description: "Enables showing recently used drilldowns or recommendations given by the datasource in the AdHocFilters and GroupBy variables", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "perPanelNonApplicableDrilldowns", Description: "Enables viewing non-applicable drilldowns on a panel level", @@ -688,6 +681,14 @@ var ( HideFromDocs: true, RequiresRestart: true, }, + { + Name: "auditLoggingAppPlatform", + Description: "Enable audit logging with Kubernetes under app platform", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, + RequiresRestart: true, + }, { Name: "secretsManagementAppPlatform", Description: "Enable the secrets management API and services under app platform", @@ -813,6 +814,13 @@ var ( Owner: grafanaDashboardsSquad, HideFromDocs: true, }, + { + Name: "dashboardAdHocAndGroupByWrapper", + Description: "Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, + }, { Name: "cloudWatchNewLabelParsing", Description: "Updates CloudWatch label parsing to be more accurate", @@ -878,6 +886,13 @@ var ( Owner: grafanaAlertingSquad, FrontendOnly: true, }, + { + Name: "alertingSavedSearches", + Description: "Enables saved searches for alert rules list", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: true, + }, { Name: "alertingDisableSendAlertsExternal", Description: "Disables the ability to send alerts to an external Alertmanager datasource.", @@ -1353,6 +1368,13 @@ var ( Owner: grafanaAlertingSquad, HideFromDocs: true, }, + { + Name: "fetchRulesInCompactMode", + Description: "Add compact=true when fetching rules", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + }, { Name: "newLogsPanel", Description: "Enables the new logs panel", @@ -1624,6 +1646,14 @@ var ( Owner: grafanaFrontendSearchNavOrganise, Expression: "false", }, + { + Name: "recentlyViewedDashboards", + Description: "Enables recently viewed dashboards section in the browsing dashboard page", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + FrontendOnly: true, + Expression: "false", + }, { Name: "alertEnrichment", Description: "Enable configuration of alert enrichments in Grafana Cloud.", @@ -1899,6 +1929,14 @@ var ( Owner: grafanaDatavizSquad, Expression: "false", }, + { + Name: "heatmapRowsAxisOptions", + Description: "Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows)", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, + Expression: "false", + }, { Name: "preventPanelChromeOverflow", Description: "Restrict PanelChrome contents with overflow: hidden;", @@ -1938,6 +1976,14 @@ var ( Owner: identityAccessTeam, Expression: "true", }, + { + Name: "pluginInsights", + Description: "Show insights for plugins in the plugin details page", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaPluginsPlatformSquad, + Expression: "false", + }, { Name: "panelTimeSettings", Description: "Enables a new panel time settings drawer", @@ -1947,6 +1993,13 @@ var ( RequiresRestart: false, HideFromDocs: false, }, + { + Name: "elasticsearchRawDSLQuery", + Description: "Enables the raw DSL query editor in the Elasticsearch data source", + Stage: FeatureStageExperimental, + Owner: grafanaPartnerPluginsSquad, + Expression: "false", + }, { Name: "kubernetesAnnotations", Description: "Enables app platform API for annotations", @@ -2015,6 +2068,13 @@ var ( FrontendOnly: true, Expression: "false", }, + { + Name: "multiPropsVariables", + Description: "Enables support for variables whose values can have multiple properties", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 3f34ca9edc9..c924b05d7d5 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -244,7 +244,6 @@ addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,6 pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan ssoSettingsApi,2023-11-08T09:50:01Z,2025-07-03T08:53:33Z,5285e9503be5702680acb2b52a6bda0632f4603d,Misi -logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki flameGraphItemCollapsing,2023-11-09T14:31:07Z,2024-07-15T12:45:41Z,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 15d15a986a5..0c85021cff8 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -24,7 +24,6 @@ influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,fa influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,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 @@ -56,6 +55,7 @@ kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true, useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false kubernetesCorrelations,experimental,@grafana/datapro,false,true,false +kubernetesUnifiedStorageQuotas,experimental,@grafana/search-and-storage,false,true,false kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false kubernetesQueryCaching,experimental,@grafana/grafana-operator-experience-squad,false,true,false dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false @@ -67,7 +67,6 @@ queryService,experimental,@grafana/grafana-datasources-core-services,false,true, queryServiceWithConnections,experimental,@grafana/grafana-datasources-core-services,false,true,false queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true -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 alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false @@ -80,9 +79,9 @@ dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false -kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true +drilldownRecommendations,experimental,@grafana/dashboards-squad,false,false,true perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true panelGroupBy,experimental,@grafana/dashboards-squad,false,false,true perPanelFiltering,experimental,@grafana/dashboards-squad,false,false,true @@ -95,6 +94,7 @@ kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false +auditLoggingAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,true,false secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,false,false secretsManagementAppPlatformUI,experimental,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false @@ -112,6 +112,7 @@ scopeFilters,experimental,@grafana/dashboards-squad,false,false,false oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false refreshTokenRequired,experimental,@grafana/identity-access-team,false,false,false newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,false,false +dashboardAdHocAndGroupByWrapper,experimental,@grafana/dashboards-squad,false,false,false cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false @@ -121,6 +122,7 @@ suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false dashboardTemplates,preview,@grafana/sharing-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true +alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,false @@ -187,6 +189,7 @@ grafanaAdvisor,privatePreview,@grafana/plugins-platform-backend,false,false,fals elasticsearchImprovedParsing,experimental,@grafana/partner-datasources,false,false,false datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false +fetchRulesInCompactMode,experimental,@grafana/alerting-squad,false,false,false 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 @@ -223,6 +226,7 @@ kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,fals kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false +recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true alertEnrichment,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false @@ -258,12 +262,15 @@ pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false, newGauge,experimental,@grafana/dataviz-squad,false,false,true newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true +heatmapRowsAxisOptions,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 newPanelPadding,preview,@grafana/dashboards-squad,false,false,true onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false +pluginInsights,experimental,@grafana/plugins-platform-backend,false,false,true panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false +elasticsearchRawDSLQuery,experimental,@grafana/partner-datasources,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true @@ -273,3 +280,4 @@ lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,false useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true +multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index afc599d4eb8..5de71954e2f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -79,10 +79,6 @@ const ( // Disables dataplane specific processing in server side expressions. FlagDisableSSEDataplane = "disableSSEDataplane" - // FlagUnifiedRequestLog - // Writes error logs to the request logger - FlagUnifiedRequestLog = "unifiedRequestLog" - // FlagRenderAuthJWT // Uses JWT-based auth for rendering instead of relying on remote cache FlagRenderAuthJWT = "renderAuthJWT" @@ -183,6 +179,10 @@ const ( // Adds support for Kubernetes correlations FlagKubernetesCorrelations = "kubernetesCorrelations" + // FlagKubernetesUnifiedStorageQuotas + // Adds support for Kubernetes unified storage quotas + FlagKubernetesUnifiedStorageQuotas = "kubernetesUnifiedStorageQuotas" + // FlagKubernetesLogsDrilldown // Adds support for Kubernetes logs drilldown FlagKubernetesLogsDrilldown = "kubernetesLogsDrilldown" @@ -259,10 +259,6 @@ const ( // Enables experimental new dashboard layouts FlagDashboardNewLayouts = "dashboardNewLayouts" - // FlagKubernetesDashboardsV2 - // Use the v2 kubernetes API in the frontend for dashboards - FlagKubernetesDashboardsV2 = "kubernetesDashboardsV2" - // FlagPdfTables // Enables generating table data as PDF in reporting FlagPdfTables = "pdfTables" @@ -279,6 +275,10 @@ const ( // Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled. FlagJitterAlertRulesWithinGroups = "jitterAlertRulesWithinGroups" + // FlagAuditLoggingAppPlatform + // Enable audit logging with Kubernetes under app platform + FlagAuditLoggingAppPlatform = "auditLoggingAppPlatform" + // FlagSecretsManagementAppPlatform // Enable the secrets management API and services under app platform FlagSecretsManagementAppPlatform = "secretsManagementAppPlatform" @@ -335,6 +335,10 @@ const ( // Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering. FlagNewDashboardWithFiltersAndGroupBy = "newDashboardWithFiltersAndGroupBy" + // FlagDashboardAdHocAndGroupByWrapper + // Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it + FlagDashboardAdHocAndGroupByWrapper = "dashboardAdHocAndGroupByWrapper" + // FlagCloudWatchNewLabelParsing // Updates CloudWatch label parsing to be more accurate FlagCloudWatchNewLabelParsing = "cloudWatchNewLabelParsing" @@ -567,6 +571,10 @@ const ( // Use a POST request to list rules by passing down the namespaces user has access to FlagFetchRulesUsingPost = "fetchRulesUsingPost" + // FlagFetchRulesInCompactMode + // Add compact=true when fetching rules + FlagFetchRulesInCompactMode = "fetchRulesInCompactMode" + // FlagGrafanaconThemes // Enables the temporary themes for GrafanaCon FlagGrafanaconThemes = "grafanaconThemes" @@ -754,6 +762,10 @@ const ( // Enables a new panel time settings drawer FlagPanelTimeSettings = "panelTimeSettings" + // FlagElasticsearchRawDSLQuery + // Enables the raw DSL query editor in the Elasticsearch data source + FlagElasticsearchRawDSLQuery = "elasticsearchRawDSLQuery" + // 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 96ebe6dc5de..6d55a6ca617 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -498,6 +498,19 @@ "codeowner": "@grafana/alerting-squad" } }, + { + "metadata": { + "name": "alertingSavedSearches", + "resourceVersion": "1765453147546", + "creationTimestamp": "2025-12-11T11:39:07Z" + }, + "spec": { + "description": "Enables saved searches for alert rules list", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true + } + }, { "metadata": { "name": "alertingTriage", @@ -645,6 +658,20 @@ "frontend": true } }, + { + "metadata": { + "name": "auditLoggingAppPlatform", + "resourceVersion": "1767013056996", + "creationTimestamp": "2025-12-29T12:57:36Z" + }, + "spec": { + "description": "Enable audit logging with Kubernetes under app platform", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad", + "requiresRestart": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "authZGRPCServer", @@ -922,6 +949,19 @@ "frontend": true } }, + { + "metadata": { + "name": "dashboardAdHocAndGroupByWrapper", + "resourceVersion": "1765841806645", + "creationTimestamp": "2025-12-15T23:36:46Z" + }, + "spec": { + "description": "Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "hideFromDocs": true + } + }, { "metadata": { "name": "dashboardDisableSchemaValidationV1", @@ -1181,6 +1221,19 @@ "codeowner": "@grafana/grafana-datasources-core-services" } }, + { + "metadata": { + "name": "drilldownRecommendations", + "resourceVersion": "1764855550769", + "creationTimestamp": "2025-12-04T13:39:10Z" + }, + "spec": { + "description": "Enables showing recently used drilldowns or recommendations given by the datasource in the AdHocFilters and GroupBy variables", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "elasticsearchCrossClusterSearch", @@ -1206,6 +1259,19 @@ "codeowner": "@grafana/partner-datasources" } }, + { + "metadata": { + "name": "elasticsearchRawDSLQuery", + "resourceVersion": "1763508396079", + "creationTimestamp": "2025-11-18T23:26:36Z" + }, + "spec": { + "description": "Enables the raw DSL query editor in the Elasticsearch data source", + "stage": "experimental", + "codeowner": "@grafana/partner-datasources", + "expression": "false" + } + }, { "metadata": { "name": "enableAppChromeExtensions", @@ -1431,6 +1497,19 @@ "expression": "false" } }, + { + "metadata": { + "name": "fetchRulesInCompactMode", + "resourceVersion": "1766045288124", + "creationTimestamp": "2025-12-18T08:08:08Z" + }, + "spec": { + "description": "Add compact=true when fetching rules", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromDocs": true + } + }, { "metadata": { "name": "fetchRulesUsingPost", @@ -1595,6 +1674,20 @@ "codeowner": "@grafana/search-and-storage" } }, + { + "metadata": { + "name": "heatmapRowsAxisOptions", + "resourceVersion": "1765353244400", + "creationTimestamp": "2025-12-10T07:54:04Z" + }, + "spec": { + "description": "Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows)", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "improvedExternalSessionHandling", @@ -1924,8 +2017,9 @@ { "metadata": { "name": "kubernetesDashboardsV2", - "resourceVersion": "1764664939750", - "creationTimestamp": "2025-12-02T08:42:19Z" + "resourceVersion": "1764236054307", + "creationTimestamp": "2025-11-27T09:34:14Z", + "deletionTimestamp": "2025-12-05T13:43:57Z" }, "spec": { "description": "Use the v2 kubernetes API in the frontend for dashboards", @@ -2039,6 +2133,19 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "kubernetesUnifiedStorageQuotas", + "resourceVersion": "1764965198011", + "creationTimestamp": "2025-12-05T20:06:38Z" + }, + "spec": { + "description": "Adds support for Kubernetes unified storage quotas", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "requiresRestart": true + } + }, { "metadata": { "name": "localeFormatPreference", @@ -2259,6 +2366,19 @@ "codeowner": "@grafana/alerting-squad" } }, + { + "metadata": { + "name": "multiPropsVariables", + "resourceVersion": "1765293230587", + "creationTimestamp": "2025-12-09T15:13:50Z" + }, + "spec": { + "description": "Enables support for variables whose values can have multiple properties", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "multiTenantTempCredentials", @@ -2628,6 +2748,20 @@ "expression": "false" } }, + { + "metadata": { + "name": "pluginInsights", + "resourceVersion": "1761300628147", + "creationTimestamp": "2025-10-24T10:10:28Z" + }, + "spec": { + "description": "Show insights for plugins in the plugin details page", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "pluginInstallAPISync", @@ -2858,7 +2992,8 @@ "metadata": { "name": "queryServiceFromExplore", "resourceVersion": "1764664939750", - "creationTimestamp": "2025-04-02T10:00:33Z" + "creationTimestamp": "2025-04-02T10:00:33Z", + "deletionTimestamp": "2025-12-10T20:33:21Z" }, "spec": { "description": "Routes explore requests to the new query service", @@ -2907,6 +3042,23 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "recentlyViewedDashboards", + "resourceVersion": "1765227168175", + "creationTimestamp": "2025-12-08T20:44:44Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-12-08 20:52:48.175129 +0000 UTC" + } + }, + "spec": { + "description": "Enables recently viewed dashboards section in the browsing dashboard page", + "stage": "experimental", + "codeowner": "@grafana/grafana-search-navigate-organise", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "refactorVariablesTimeRange", @@ -3405,7 +3557,8 @@ "metadata": { "name": "unifiedRequestLog", "resourceVersion": "1764664939750", - "creationTimestamp": "2023-03-31T13:38:09Z" + "creationTimestamp": "2023-03-31T13:38:09Z", + "deletionTimestamp": "2025-12-18T14:21:02Z" }, "spec": { "description": "Writes error logs to the request logger", diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 1551d858efe..69b8cb59311 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -202,6 +202,11 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S if query.Title != "" { // allow wildcard search request.Query = "*" + strings.ToLower(query.Title) + "*" + // or perform exact match if requested + if query.TitleExactMatch { + request.Query = query.Title + } + // if using query, you need to specify the fields you want request.Fields = dashboardsearch.IncludeFields } @@ -721,19 +726,6 @@ func (s *Service) moveOnApiServer(ctx context.Context, cmd *folder.MoveFolderCom return nil, folder.ErrBadRequest.Errorf("k6 project may not be moved") } - f, err := s.unifiedStore.Get(ctx, folder.GetFolderQuery{ - UID: &cmd.UID, - OrgID: cmd.OrgID, - SignedInUser: cmd.SignedInUser, - }) - if err != nil { - return nil, err - } - - if f != nil && f.ParentUID == accesscontrol.K6FolderUID { - return nil, folder.ErrBadRequest.Errorf("k6 project may not be moved") - } - // Check that the user is allowed to move the folder to the destination folder hasAccess, evalErr := s.canMoveViaApiServer(ctx, cmd) if evalErr != nil { @@ -743,30 +735,7 @@ func (s *Service) moveOnApiServer(ctx context.Context, cmd *folder.MoveFolderCom return nil, dashboards.ErrFolderAccessDenied } - // here we get the folder, we need to get the height of current folder - // and the depth of the new parent folder, the sum can't bypass 8 - folderHeight, err := s.unifiedStore.GetHeight(ctx, cmd.UID, cmd.OrgID, &cmd.NewParentUID) - if err != nil { - return nil, err - } - parents, err := s.unifiedStore.GetParents(ctx, folder.GetParentsQuery{UID: cmd.NewParentUID, OrgID: cmd.OrgID}) - if err != nil { - return nil, err - } - - // height of the folder that is being moved + this current folder itself + depth of the NewParent folder should be less than or equal MaxNestedFolderDepth - if folderHeight+len(parents)+1 > folder.MaxNestedFolderDepth { - return nil, folder.ErrMaximumDepthReached.Errorf("failed to move folder") - } - - for _, parent := range parents { - // if the current folder is already a parent of newparent, we should return error - if parent.UID == cmd.UID { - return nil, folder.ErrCircularReference.Errorf("failed to move folder") - } - } - - f, err = s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{ + f, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{ UID: cmd.UID, OrgID: cmd.OrgID, NewParentUID: &cmd.NewParentUID, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 0ff7a25b0e9..3a47c41fffb 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -439,7 +439,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { ID: fooFolder.ID, // nolint:staticcheck UID: fooFolder.UID, }, - }, nil).Once() + }, nil).Twice() // Called twice due to total count call id := int64(123) emptyString := "" query := &folder.GetFolderQuery{ @@ -455,7 +455,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by non existing ID should return not found error", func(t *testing.T) { - dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Twice() // Called twice due to total count call id := int64(111111) query := &folder.GetFolderQuery{ ID: &id, @@ -475,7 +475,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { ID: fooFolder.ID, // nolint:staticcheck UID: fooFolder.UID, }, - }, nil).Once() + }, nil).Twice() // Called twice due to total count call title := "foo" query := &folder.GetFolderQuery{ Title: &title, @@ -489,7 +489,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) t.Run("When get folder by non existing Title should return not found error", func(t *testing.T) { - dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once() + dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Twice() // Called twice due to total count call title := "does not exists" query := &folder.GetFolderQuery{ Title: &title, diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 3e59f5c1b6f..e0061ca8dd7 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -224,12 +224,13 @@ type GetFoldersQuery struct { } type SearchFoldersQuery struct { - OrgID int64 - UIDs []string - IDs []int64 - Title string - Limit int64 - SignedInUser identity.Requester `json:"-"` + OrgID int64 + UIDs []string + IDs []int64 + Title string + TitleExactMatch bool + Limit int64 + SignedInUser identity.Requester `json:"-"` } // GetParentsQuery captures the information required by the folder service to diff --git a/pkg/services/frontend/frontend_service.go b/pkg/services/frontend/frontend_service.go index f730bfba376..74776a1169e 100644 --- a/pkg/services/frontend/frontend_service.go +++ b/pkg/services/frontend/frontend_service.go @@ -22,6 +22,7 @@ import ( fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" + publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -133,7 +134,7 @@ func (s *frontendService) addMiddlewares(m *web.Mux) { loggermiddleware := loggermw.Provide(s.cfg, s.features) m.Use(requestmeta.SetupRequestMetadata()) - m.Use(middleware.RequestTracing(s.tracer, middleware.TraceAllPaths)) + m.Use(middleware.RequestTracing(s.tracer, middleware.ShouldTraceAllPaths)) m.Use(middleware.RequestMetrics(s.features, s.cfg, s.promRegister)) m.UseMiddleware(s.contextMiddleware()) @@ -164,6 +165,11 @@ func (s *frontendService) registerRoutes(m *web.Mux) { // uses cache busting to ensure requests aren't cached. s.routeGet(m, "/-/fe-boot-error", s.handleBootError) + s.routeGet(m, "/public-dashboards/:accessToken", + publicdashboardsapi.SetPublicDashboardAccessToken, + s.index.HandleRequest, + ) + // All other requests return index.html s.routeGet(m, "/*", s.index.HandleRequest) } diff --git a/pkg/services/frontend/frontend_settings.go b/pkg/services/frontend/frontend_settings.go index f6cf9855b2d..d9476c8ff40 100644 --- a/pkg/services/frontend/frontend_settings.go +++ b/pkg/services/frontend/frontend_settings.go @@ -32,6 +32,7 @@ type FSFrontendSettings struct { RudderstackWriteKey string `json:"rudderstackWriteKey,omitempty"` RudderstackDataPlaneUrl string `json:"rudderstackDataPlaneUrl,omitempty"` RudderstackSdkUrl string `json:"rudderstackSdkUrl,omitempty"` + RudderstackV3SdkUrl string `json:"rudderstackV3SdkUrl,omitempty"` RudderstackConfigUrl string `json:"rudderstackConfigUrl,omitempty"` RudderstackIntegrationsUrl string `json:"rudderstackIntegrationsUrl,omitempty"` diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 22d04234a36..25704f1ab6d 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -45,6 +45,8 @@ type IndexViewData struct { // Nonce is a cryptographic identifier for use with Content Security Policy. Nonce string + + PublicDashboardAccessToken string } // Templates setup. @@ -92,6 +94,7 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li RudderstackDataPlaneUrl: cfg.RudderstackDataPlaneURL, RudderstackIntegrationsUrl: cfg.RudderstackIntegrationsURL, RudderstackSdkUrl: cfg.RudderstackSDKURL, + RudderstackV3SdkUrl: cfg.RudderstackV3SDKURL, 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, @@ -138,9 +141,12 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. return } + reqCtx := contexthandler.FromContext(ctx) + // TODO -- restructure so the static stuff is under one variable and the rest is dynamic data := p.data // copy everything data.Nonce = nonce + data.PublicDashboardAccessToken = reqCtx.PublicDashboardAccessToken if data.CSPEnabled { data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce) @@ -150,7 +156,6 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. writer.Header().Set("Content-Security-Policy-Report-Only", policy) } - reqCtx := contexthandler.FromContext(ctx) p.runIndexDataHooks(reqCtx, &data) writer.Header().Set("Content-Type", "text/html; charset=UTF-8") diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index b0364fcbac9..198b8216189 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -188,6 +188,7 @@ // Wrap in an IIFE to avoid polluting the global scope. Intentionally global-scope properties // are explicitly assigned to the `window` object. (() => { + const publicDashboardAccessToken = [[.PublicDashboardAccessToken]] // Grafana can only fail to load once // However, it can fail to load in multiple different places // To avoid double reporting the error, we use this boolean to check if we've already failed @@ -271,9 +272,15 @@ async function fetchBootData() { const queryParams = new URLSearchParams(window.location.search); + let path = '/bootdata'; + // call a special bootdata url with the public access token + // this is needed to set the access token and correct org for public dashboards on the ST backend + if (publicDashboardAccessToken) { + path += `/${publicDashboardAccessToken}`; + } // pass the search params through to the bootdata request // this allows for overriding the theme/language etc - const bootDataUrl = new URL('/bootdata', window.location.origin); + const bootDataUrl = new URL(path, window.location.origin); for (const [key, value] of queryParams.entries()) { bootDataUrl.searchParams.append(key, value); } diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 4895e7e6505..df51717756e 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -424,6 +424,9 @@ func (l *LibraryElementService) toLibraryElementError(err error, message string) if errors.Is(err, model.ErrLibraryElementUIDTooLong) { return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err) } + if errors.Is(err, model.ErrLibraryElementProvisionedFolder) { + return response.Error(http.StatusConflict, model.ErrLibraryElementProvisionedFolder.Error(), err) + } if err != nil && strings.Contains(err.Error(), "insufficient permissions") { return response.Error(http.StatusForbidden, err.Error(), err) } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index a5b43fd54cb..2baa27ac1f8 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/metrics" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -125,6 +126,20 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id } } + if cmd.FolderUID != nil { + f, err := l.folderService.Get(c, &folder.GetFolderQuery{ + OrgID: signedInUser.GetOrgID(), + UID: cmd.FolderUID, + SignedInUser: signedInUser, + }) + if err != nil { + return model.LibraryElementDTO{}, err + } + if f.ManagedBy == utils.ManagerKindRepo { + return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder + } + } + updatedModel := cmd.Model var err error if cmd.Kind == int64(model.PanelElement) { @@ -601,6 +616,21 @@ func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInU if err := l.requireSupportedElementKind(cmd.Kind); err != nil { return model.LibraryElementDTO{}, err } + + if cmd.FolderUID != nil { + f, err := l.folderService.Get(c, &folder.GetFolderQuery{ + OrgID: signedInUser.GetOrgID(), + UID: cmd.FolderUID, + SignedInUser: signedInUser, + }) + if err != nil { + return model.LibraryElementDTO{}, err + } + if f.ManagedBy == utils.ManagerKindRepo { + return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder + } + } + err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 6e2bdfdcc41..4868bf50cbf 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -161,6 +161,8 @@ var ( ErrLibraryElementInvalidUID = errors.New("uid contains illegal characters") // errLibraryElementUIDTooLong is an error for when the uid of a library element is invalid ErrLibraryElementUIDTooLong = errors.New("uid too long, max 40 characters") + // ErrLibraryElementProvisionedFolder indicates that a library element cannot be created on a provisioned folder. + ErrLibraryElementProvisionedFolder = errors.New("resource type not supported in repository-managed folders") ) // Commands diff --git a/pkg/services/live/database/storage.go b/pkg/services/live/database/storage.go deleted file mode 100644 index 3736c27d265..00000000000 --- a/pkg/services/live/database/storage.go +++ /dev/null @@ -1,48 +0,0 @@ -package database - -import ( - "fmt" - "time" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/services/live/model" -) - -type Storage struct { - store db.DB - cache *localcache.CacheService -} - -func NewStorage(store db.DB, cache *localcache.CacheService) *Storage { - return &Storage{store: store, cache: cache} -} - -func getLiveMessageCacheKey(orgID int64, channel string) string { - return fmt.Sprintf("live_message_%d_%s", orgID, channel) -} - -func (s *Storage) SaveLiveMessage(query *model.SaveLiveMessageQuery) error { - // Come back to saving into database after evaluating database structure. - s.cache.Set(getLiveMessageCacheKey(query.OrgID, query.Channel), model.LiveMessage{ - ID: 0, // Not used actually. - OrgID: query.OrgID, - Channel: query.Channel, - Data: query.Data, - Published: time.Now(), - }, 0) - return nil -} - -func (s *Storage) GetLiveMessage(query *model.GetLiveMessageQuery) (model.LiveMessage, bool, error) { - // Come back to saving into database after evaluating database structure. - m, ok := s.cache.Get(getLiveMessageCacheKey(query.OrgID, query.Channel)) - if !ok { - return model.LiveMessage{}, false, nil - } - msg, ok := m.(model.LiveMessage) - if !ok { - return model.LiveMessage{}, false, fmt.Errorf("unexpected live message type in cache: %T", m) - } - return msg, true, nil -} diff --git a/pkg/services/live/database/tests/setup.go b/pkg/services/live/database/tests/setup.go deleted file mode 100644 index 9ebf8f84a26..00000000000 --- a/pkg/services/live/database/tests/setup.go +++ /dev/null @@ -1,18 +0,0 @@ -package tests - -import ( - "testing" - "time" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/services/live/database" -) - -// SetupTestStorage initializes a storage to used by the integration tests. -// This is required to properly register and execute migrations. -func SetupTestStorage(t *testing.T) *database.Storage { - sqlStore := db.InitTestDB(t) - localCache := localcache.New(time.Hour, time.Hour) - return database.NewStorage(sqlStore, localCache) -} diff --git a/pkg/services/live/database/tests/storage_test.go b/pkg/services/live/database/tests/storage_test.go deleted file mode 100644 index 0c57ec08e50..00000000000 --- a/pkg/services/live/database/tests/storage_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package tests - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/tests/testsuite" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationLiveMessage(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - storage := SetupTestStorage(t) - - getQuery := &model.GetLiveMessageQuery{ - OrgID: 1, - Channel: "test_channel", - } - _, ok, err := storage.GetLiveMessage(getQuery) - require.NoError(t, err) - require.False(t, ok) - - saveQuery := &model.SaveLiveMessageQuery{ - OrgID: 1, - Channel: "test_channel", - Data: []byte(`{}`), - } - err = storage.SaveLiveMessage(saveQuery) - require.NoError(t, err) - - msg, ok, err := storage.GetLiveMessage(getQuery) - require.NoError(t, err) - require.True(t, ok) - require.Equal(t, int64(1), msg.OrgID) - require.Equal(t, "test_channel", msg.Channel) - require.Equal(t, json.RawMessage(`{}`), msg.Data) - require.NotZero(t, msg.Published) - - // try saving again, should be replaced. - saveQuery2 := &model.SaveLiveMessageQuery{ - OrgID: 1, - Channel: "test_channel", - Data: []byte(`{"input": "hello"}`), - } - err = storage.SaveLiveMessage(saveQuery2) - require.NoError(t, err) - - getQuery2 := &model.GetLiveMessageQuery{ - OrgID: 1, - Channel: "test_channel", - } - msg2, ok, err := storage.GetLiveMessage(getQuery2) - require.NoError(t, err) - require.True(t, ok) - require.Equal(t, int64(1), msg2.OrgID) - require.Equal(t, "test_channel", msg2.Channel) - require.Equal(t, json.RawMessage(`{"input": "hello"}`), msg2.Data) - require.NotZero(t, msg2.Published) -} diff --git a/pkg/services/live/features/broadcast.go b/pkg/services/live/features/broadcast.go deleted file mode 100644 index fc9bbc56f2b..00000000000 --- a/pkg/services/live/features/broadcast.go +++ /dev/null @@ -1,70 +0,0 @@ -package features - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/live/model" -) - -var ( - logger = log.New("live.features") // scoped to all features? -) - -//go:generate mockgen -destination=broadcast_mock.go -package=features github.com/grafana/grafana/pkg/services/live/features LiveMessageStore - -type LiveMessageStore interface { - SaveLiveMessage(query *model.SaveLiveMessageQuery) error - GetLiveMessage(query *model.GetLiveMessageQuery) (model.LiveMessage, bool, error) -} - -// BroadcastRunner will simply broadcast all events to `grafana/broadcast/*` channels -// This assumes that data is a JSON object -type BroadcastRunner struct { - liveMessageStore LiveMessageStore -} - -func NewBroadcastRunner(liveMessageStore LiveMessageStore) *BroadcastRunner { - return &BroadcastRunner{liveMessageStore: liveMessageStore} -} - -// GetHandlerForPath called on init -func (b *BroadcastRunner) GetHandlerForPath(_ string) (model.ChannelHandler, error) { - return b, nil // all dashboards share the same handler -} - -// OnSubscribe will let anyone connect to the path -func (b *BroadcastRunner) OnSubscribe(_ context.Context, u identity.Requester, e model.SubscribeEvent) (model.SubscribeReply, backend.SubscribeStreamStatus, error) { - reply := model.SubscribeReply{ - Presence: true, - JoinLeave: true, - } - query := &model.GetLiveMessageQuery{ - OrgID: u.GetOrgID(), - Channel: e.Channel, - } - msg, ok, err := b.liveMessageStore.GetLiveMessage(query) - if err != nil { - return model.SubscribeReply{}, 0, err - } - if ok { - reply.Data = msg.Data - } - return reply, backend.SubscribeStreamStatusOK, nil -} - -// OnPublish is called when a client wants to broadcast on the websocket -func (b *BroadcastRunner) OnPublish(_ context.Context, u identity.Requester, e model.PublishEvent) (model.PublishReply, backend.PublishStreamStatus, error) { - query := &model.SaveLiveMessageQuery{ - OrgID: u.GetOrgID(), - Channel: e.Channel, - Data: e.Data, - } - if err := b.liveMessageStore.SaveLiveMessage(query); err != nil { - return model.PublishReply{}, 0, err - } - return model.PublishReply{Data: e.Data}, backend.PublishStreamStatusOK, nil -} diff --git a/pkg/services/live/features/broadcast_mock.go b/pkg/services/live/features/broadcast_mock.go deleted file mode 100644 index 54df3472a98..00000000000 --- a/pkg/services/live/features/broadcast_mock.go +++ /dev/null @@ -1,66 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/grafana/grafana/pkg/services/live/features (interfaces: LiveMessageStore) - -// Package features is a generated GoMock package. -package features - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - - model "github.com/grafana/grafana/pkg/services/live/model" -) - -// MockLiveMessageStore is a mock of LiveMessageStore interface. -type MockLiveMessageStore struct { - ctrl *gomock.Controller - recorder *MockLiveMessageStoreMockRecorder -} - -// MockLiveMessageStoreMockRecorder is the mock recorder for MockLiveMessageStore. -type MockLiveMessageStoreMockRecorder struct { - mock *MockLiveMessageStore -} - -// NewMockLiveMessageStore creates a new mock instance. -func NewMockLiveMessageStore(ctrl *gomock.Controller) *MockLiveMessageStore { - mock := &MockLiveMessageStore{ctrl: ctrl} - mock.recorder = &MockLiveMessageStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockLiveMessageStore) EXPECT() *MockLiveMessageStoreMockRecorder { - return m.recorder -} - -// GetLiveMessage mocks base method. -func (m *MockLiveMessageStore) GetLiveMessage(arg0 *model.GetLiveMessageQuery) (model.LiveMessage, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLiveMessage", arg0) - ret0, _ := ret[0].(model.LiveMessage) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetLiveMessage indicates an expected call of GetLiveMessage. -func (mr *MockLiveMessageStoreMockRecorder) GetLiveMessage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLiveMessage", reflect.TypeOf((*MockLiveMessageStore)(nil).GetLiveMessage), arg0) -} - -// SaveLiveMessage mocks base method. -func (m *MockLiveMessageStore) SaveLiveMessage(arg0 *model.SaveLiveMessageQuery) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveLiveMessage", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveLiveMessage indicates an expected call of SaveLiveMessage. -func (mr *MockLiveMessageStoreMockRecorder) SaveLiveMessage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveLiveMessage", reflect.TypeOf((*MockLiveMessageStore)(nil).SaveLiveMessage), arg0) -} diff --git a/pkg/services/live/features/broadcast_test.go b/pkg/services/live/features/broadcast_test.go deleted file mode 100644 index 25065087420..00000000000 --- a/pkg/services/live/features/broadcast_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package features - -import ( - "context" - "encoding/json" - "testing" - - "github.com/golang/mock/gomock" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/services/user" -) - -func TestNewBroadcastRunner(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - d := NewMockLiveMessageStore(mockCtrl) - br := NewBroadcastRunner(d) - require.NotNil(t, br) -} - -func TestBroadcastRunner_OnSubscribe(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockDispatcher := NewMockLiveMessageStore(mockCtrl) - - channel := "stream/channel/test" - data := json.RawMessage(`{}`) - - mockDispatcher.EXPECT().GetLiveMessage(&model.GetLiveMessageQuery{ - OrgID: 1, - Channel: channel, - }).DoAndReturn(func(query *model.GetLiveMessageQuery) (model.LiveMessage, bool, error) { - return model.LiveMessage{ - Data: data, - }, true, nil - }).Times(1) - - br := NewBroadcastRunner(mockDispatcher) - require.NotNil(t, br) - handler, err := br.GetHandlerForPath("test") - require.NoError(t, err) - reply, status, err := handler.OnSubscribe( - context.Background(), - &user.SignedInUser{OrgID: 1, UserID: 2}, - model.SubscribeEvent{Channel: channel, Path: "test"}, - ) - require.NoError(t, err) - require.Equal(t, backend.SubscribeStreamStatusOK, status) - require.Equal(t, data, reply.Data) - require.True(t, reply.Presence) - require.True(t, reply.JoinLeave) - require.False(t, reply.Recover) -} - -func TestBroadcastRunner_OnPublish(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - mockDispatcher := NewMockLiveMessageStore(mockCtrl) - - channel := "stream/channel/test" - data := json.RawMessage(`{}`) - var orgID int64 = 1 - - mockDispatcher.EXPECT().SaveLiveMessage(&model.SaveLiveMessageQuery{ - OrgID: orgID, - Channel: channel, - Data: data, - }).DoAndReturn(func(query *model.SaveLiveMessageQuery) error { - return nil - }).Times(1) - - br := NewBroadcastRunner(mockDispatcher) - require.NotNil(t, br) - handler, err := br.GetHandlerForPath("test") - require.NoError(t, err) - reply, status, err := handler.OnPublish( - context.Background(), - &user.SignedInUser{OrgID: 1, UserID: 2}, - model.PublishEvent{Channel: channel, Path: "test", Data: data}, - ) - require.NoError(t, err) - require.Equal(t, backend.PublishStreamStatusOK, status) - require.Equal(t, data, reply.Data) -} diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index 286dad7c49c..bb51635ef3d 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -6,11 +6,11 @@ import ( "fmt" "strings" + "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/live/model" ) @@ -22,48 +22,21 @@ const ( ActionDeleted actionType = "deleted" EditingStarted actionType = "editing-started" //EditingFinished actionType = "editing-finished" - - GitopsChannel = "grafana/dashboard/gitops" ) -type userDisplayDTO struct { - ID int64 `json:"id,omitempty"` - UID string `json:"uid,omitempty"` - Name string `json:"name,omitempty"` - Login string `json:"login,omitempty"` - AvatarURL string `json:"avatarUrl"` -} - -// Static function to parse a requester into a userDisplayDTO -func newUserDisplayDTOFromRequester(requester identity.Requester) *userDisplayDTO { - // nolint:staticcheck - userID, _ := requester.GetInternalID() - return &userDisplayDTO{ - ID: userID, - UID: requester.GetRawIdentifier(), - Login: requester.GetLogin(), - Name: requester.GetName(), - } -} - // DashboardEvent events related to dashboards type dashboardEvent struct { - UID string `json:"uid"` - Action actionType `json:"action"` // saved, editing, deleted - User *userDisplayDTO `json:"user,omitempty"` - SessionID string `json:"sessionId,omitempty"` - Message string `json:"message,omitempty"` - Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"` - Error string `json:"error,omitempty"` + UID string `json:"uid"` + Action actionType `json:"action"` // saved, editing, deleted + SessionID string `json:"sessionId,omitempty"` + ResourceVersion string `json:"rv,omitempty"` } // DashboardHandler manages all the `grafana/dashboard/*` channels type DashboardHandler struct { - Publisher model.ChannelPublisher - ClientCount model.ChannelClientCount - Store db.DB - DashboardService dashboards.DashboardService - AccessControl accesscontrol.AccessControl + Publisher model.ChannelPublisher + ClientCount model.ChannelClientCount + AccessControl dashboards.DashboardAccessService } // GetHandlerForPath called on init @@ -77,23 +50,15 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user identity.Reques // make sure can view this dashboard if len(parts) == 2 && parts[0] == "uid" { - query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.GetOrgID()} - _, err := h.DashboardService.GetDashboard(ctx, &query) - if err != nil { - logger.Error("Error getting dashboard", "query", query, "error", err) - return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil + ns := types.OrgNamespaceFormatter(user.GetOrgID()) + ok, err := h.AccessControl.HasDashboardAccess(ctx, user, utils.VerbGet, ns, parts[1]) + if ok && err == nil { + return model.SubscribeReply{ + Presence: true, + JoinLeave: true, + }, backend.SubscribeStreamStatusOK, nil } - - evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1])) - canView, err := h.AccessControl.Evaluate(ctx, user, evaluator) - if err != nil || !canView { - return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err - } - - return model.SubscribeReply{ - Presence: true, - JoinLeave: true, - }, backend.SubscribeStreamStatusOK, nil + return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err } // Unknown path @@ -116,32 +81,16 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Req // just ignore the event return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") } - query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: requester.GetOrgID()} - _, err = h.DashboardService.GetDashboard(ctx, &query) - if err != nil { - logger.Error("Unknown dashboard", "query", query) - return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil - } - evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1])) - canEdit, err := h.AccessControl.Evaluate(ctx, requester, evaluator) - if err != nil { - return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") + ns := types.OrgNamespaceFormatter(requester.GetOrgID()) + ok, err := h.AccessControl.HasDashboardAccess(ctx, requester, utils.VerbUpdate, ns, parts[1]) + if ok && err == nil { + msg, err := json.Marshal(event) + if err != nil { + return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") + } + return model.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil } - - // Ignore edit events if the user can not edit - if !canEdit { - return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP - } - - // Tell everyone who is editing - event.User = newUserDisplayDTOFromRequester(requester) - - msg, err := json.Marshal(event) - if err != nil { - return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") - } - return model.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil } return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil @@ -153,55 +102,22 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error { if err != nil { return err } - - // Only broadcast non-error events - if event.Error == "" { - err = h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg) - if err != nil { - return err - } - } - - // Send everything to the gitops channel - return h.Publisher(orgID, GitopsChannel, msg) + return h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg) } // DashboardSaved will broadcast to all connected dashboards -func (h *DashboardHandler) DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error { - if err != nil && !h.HasGitOpsObserver(orgID) { - return nil // only broadcast if it was OK - } - - msg := dashboardEvent{ - UID: dashboard.UID, - Action: ActionSaved, - User: newUserDisplayDTOFromRequester(requester), - Message: message, - Dashboard: dashboard, - } - - if err != nil { - msg.Error = err.Error() - } - - return h.publish(orgID, msg) -} - -// DashboardDeleted will broadcast to all connected dashboards -func (h *DashboardHandler) DashboardDeleted(orgID int64, requester identity.Requester, uid string) error { +func (h *DashboardHandler) DashboardSaved(orgID int64, uid string, rv string) error { return h.publish(orgID, dashboardEvent{ - UID: uid, - Action: ActionDeleted, - User: newUserDisplayDTOFromRequester(requester), + UID: uid, + Action: ActionSaved, + ResourceVersion: rv, }) } -// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel -func (h *DashboardHandler) HasGitOpsObserver(orgID int64) bool { - count, err := h.ClientCount(orgID, GitopsChannel) - if err != nil { - logger.Error("Error getting client count", "error", err) - return false - } - return count > 0 +// DashboardDeleted will broadcast to all connected dashboards +func (h *DashboardHandler) DashboardDeleted(orgID int64, uid string) error { + return h.publish(orgID, dashboardEvent{ + UID: uid, + Action: ActionDeleted, + }) } diff --git a/pkg/services/live/features/plugin.go b/pkg/services/live/features/plugin.go index de7a55ea2ab..1f7d0768078 100644 --- a/pkg/services/live/features/plugin.go +++ b/pkg/services/live/features/plugin.go @@ -5,9 +5,11 @@ import ( "errors" "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/live/orgchannel" diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 4d91eae3e5e..fe1dffba1b8 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -15,7 +15,6 @@ import ( "github.com/centrifugal/centrifuge" "github.com/gobwas/glob" - jsoniter "github.com/json-iterator/go" "github.com/redis/go-redis/v9" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -25,25 +24,19 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/live" - "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/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/live/database" "github.com/grafana/grafana/pkg/services/live/features" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/liveplugin" @@ -57,8 +50,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/services/query" - "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -78,30 +69,23 @@ type CoreGrafanaScope struct { Dashboards DashboardActivityChannel } -func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister, - pluginStore pluginstore.Store, pluginClient plugins.Client, cacheService *localcache.CacheService, - dataSourceCache datasources.CacheService, sqlStore db.DB, secretsService secrets.Service, - usageStatsService usagestats.Service, queryDataService query.Service, toggles featuremgmt.FeatureToggles, - accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService, - orgService org.Service, configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) { +func ProvideService(cfg *setting.Cfg, routeRegister routing.RouteRegister, plugCtxProvider *plugincontext.Provider, + pluginStore pluginstore.Store, pluginClient plugins.Client, dataSourceCache datasources.CacheService, + usageStatsService usagestats.Service, toggles featuremgmt.FeatureToggles, + dashboardService dashboards.DashboardAccessService, + configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) { g := &GrafanaLive{ Cfg: cfg, Features: toggles, PluginContextProvider: plugCtxProvider, - RouteRegister: routeRegister, pluginStore: pluginStore, pluginClient: pluginClient, - CacheService: cacheService, DataSourceCache: dataSourceCache, - SQLStore: sqlStore, - SecretsService: secretsService, - queryDataService: queryDataService, channels: make(map[string]model.ChannelHandler), GrafanaScope: CoreGrafanaScope{ Features: make(map[string]model.ChannelHandlerFactory), }, usageStatsService: usageStatsService, - orgService: orgService, keyPrefix: "gf_live", } @@ -184,22 +168,13 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r // Initialize the main features dash := &features.DashboardHandler{ - Publisher: g.Publish, - ClientCount: g.ClientCount, - Store: sqlStore, - DashboardService: dashboardService, - AccessControl: accessControl, + Publisher: g.Publish, + ClientCount: g.ClientCount, + AccessControl: dashboardService, } - g.storage = database.NewStorage(g.SQLStore, g.CacheService) g.GrafanaScope.Dashboards = dash g.GrafanaScope.Features["dashboard"] = dash - g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage) - - // Testing watch with just the provisioning support -- this will be removed when it is well validated - //nolint:staticcheck // not yet migrated to OpenFeature - if toggles.IsEnabledGlobally(featuremgmt.FlagProvisioning) { - g.GrafanaScope.Features["watch"] = features.NewWatchRunner(g.Publish, configProvider) - } + g.GrafanaScope.Features["watch"] = features.NewWatchRunner(g.Publish, configProvider) g.surveyCaller = survey.NewCaller(managedStreamRunner, node) err = g.surveyCaller.SetupHandlers() @@ -388,14 +363,14 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r UserID: strconv.FormatInt(id, 10), } newCtx := centrifuge.SetCredentials(ctx.Req.Context(), cred) - newCtx = livecontext.SetContextSignedUser(newCtx, user) + newCtx = identity.WithRequester(newCtx, user) r := ctx.Req.WithContext(newCtx) wsHandler.ServeHTTP(ctx.Resp, r) } g.pushWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser - newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) + newCtx := identity.WithRequester(ctx.Req.Context(), user) newCtx = livecontext.SetContextStreamID(newCtx, web.Params(ctx.Req)[":streamId"]) r := ctx.Req.WithContext(newCtx) pushWSHandler.ServeHTTP(ctx.Resp, r) @@ -403,17 +378,17 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r g.pushPipelineWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser - newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) + newCtx := identity.WithRequester(ctx.Req.Context(), user) newCtx = livecontext.SetContextChannelID(newCtx, web.Params(ctx.Req)["*"]) r := ctx.Req.WithContext(newCtx) pushPipelineWSHandler.ServeHTTP(ctx.Resp, r) } - g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) { + routeRegister.Group("/api/live", func(group routing.RouteRegister) { group.Get("/ws", g.websocketHandler) }, middleware.ReqSignedIn, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone)) - g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) { + routeRegister.Group("/api/live", func(group routing.RouteRegister) { group.Get("/push/:streamId", g.pushWebsocketHandler) group.Get("/pipeline/push/*", g.pushPipelineWebsocketHandler) }, middleware.ReqOrgAdmin, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone)) @@ -472,17 +447,11 @@ type GrafanaLive struct { PluginContextProvider *plugincontext.Provider Cfg *setting.Cfg Features featuremgmt.FeatureToggles - RouteRegister routing.RouteRegister - CacheService *localcache.CacheService DataSourceCache datasources.CacheService - SQLStore db.DB - SecretsService secrets.Service pluginStore pluginstore.Store pluginClient plugins.Client - queryDataService query.Service - orgService org.Service - keyPrefix string + keyPrefix string // HA prefix for grafana cloud (since the org is always 1) node *centrifuge.Node surveyCaller *survey.Caller @@ -505,7 +474,6 @@ type GrafanaLive struct { contextGetter *liveplugin.ContextGetter runStreamManager *runstream.Manager - storage *database.Storage usageStatsService usagestats.Service usageStats usageStats @@ -513,18 +481,17 @@ type GrafanaLive struct { // DashboardActivityChannel is a service to advertise dashboard activity type DashboardActivityChannel interface { - // Called when a dashboard is saved -- this includes the error so we can support a - // gitops workflow that knows if the value was saved to the local database or not - // in many cases all direct save requests will fail, but the request should be forwarded - // to any gitops observers - DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error + // Called when a dashboard is saved + DashboardSaved(orgID int64, uid string, rv string) error // Called when a dashboard is deleted - DashboardDeleted(orgID int64, requester identity.Requester, uid string) error + DashboardDeleted(orgID int64, uid string) error +} - // Experimental! Indicate is GitOps is active. This really means - // someone is subscribed to the `grafana/dashboards/gitops` channel - HasGitOpsObserver(orgID int64) bool +// ProvideDashboardActivityChannel extracts the DashboardActivityChannel from GrafanaLive. +// This is used by wire to inject the channel into the dashboard API service. +func ProvideDashboardActivityChannel(live *GrafanaLive) DashboardActivityChannel { + return live.GrafanaScope.Dashboards } func (g *GrafanaLive) getStreamPlugin(ctx context.Context, pluginID string) (backend.StreamHandler, error) { @@ -674,18 +641,13 @@ func (g *GrafanaLive) HandleDatasourceUpdate(orgID int64, dsUID string) { } } -// Use a configuration that's compatible with the standard library -// to minimize the risk of introducing bugs. This will make sure -// that map keys is ordered. -var jsonStd = jsoniter.ConfigCompatibleWithStandardLibrary - func (g *GrafanaLive) handleOnRPC(clientContextWithSpan context.Context, client *centrifuge.Client, e centrifuge.RPCEvent) (centrifuge.RPCReply, error) { logger.Debug("Client calls RPC", "user", client.UserID(), "client", client.ID(), "method", e.Method) if e.Method != "grafana.query" { return centrifuge.RPCReply{}, centrifuge.ErrorMethodNotFound } - user, ok := livecontext.GetContextSignedUser(clientContextWithSpan) - if !ok { + user, err := identity.GetRequester(clientContextWithSpan) + if err != nil { logger.Error("No user found in context", "user", client.UserID(), "client", client.ID(), "method", e.Method) return centrifuge.RPCReply{}, centrifuge.ErrorInternal } @@ -695,38 +657,15 @@ func (g *GrafanaLive) handleOnRPC(clientContextWithSpan context.Context, client return centrifuge.RPCReply{}, centrifuge.ErrorExpired } - var req dtos.MetricRequest - err := json.Unmarshal(e.Data, &req) - if err != nil { - return centrifuge.RPCReply{}, centrifuge.ErrorBadRequest - } - resp, err := g.queryDataService.QueryData(clientContextWithSpan, user, false, req) - if err != nil { - logger.Error("Error query data", "user", client.UserID(), "client", client.ID(), "method", e.Method, "error", err) - if errors.Is(err, datasources.ErrDataSourceAccessDenied) { - return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusForbidden), Message: http.StatusText(http.StatusForbidden)} - } - var gfErr errutil.Error - if errors.As(err, &gfErr) && gfErr.Reason.Status() == errutil.StatusBadRequest { - return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusBadRequest), Message: http.StatusText(http.StatusBadRequest)} - } - return centrifuge.RPCReply{}, centrifuge.ErrorInternal - } - data, err := jsonStd.Marshal(resp) - if err != nil { - logger.Error("Error marshaling query response", "user", client.UserID(), "client", client.ID(), "method", e.Method, "error", err) - return centrifuge.RPCReply{}, centrifuge.ErrorInternal - } - return centrifuge.RPCReply{ - Data: data, - }, nil + // RPC events not available + return centrifuge.RPCReply{}, centrifuge.ErrorNotAvailable } func (g *GrafanaLive) handleOnSubscribe(clientContextWithSpan context.Context, client *centrifuge.Client, e centrifuge.SubscribeEvent) (centrifuge.SubscribeReply, error) { logger.Debug("Client wants to subscribe", "user", client.UserID(), "client", client.ID(), "channel", e.Channel) - user, ok := livecontext.GetContextSignedUser(clientContextWithSpan) - if !ok { + user, err := identity.GetRequester(clientContextWithSpan) + if err != nil { logger.Error("No user found in context", "user", client.UserID(), "client", client.ID(), "channel", e.Channel) return centrifuge.SubscribeReply{}, centrifuge.ErrorInternal } @@ -831,8 +770,8 @@ func (g *GrafanaLive) handleOnSubscribe(clientContextWithSpan context.Context, c func (g *GrafanaLive) handleOnPublish(clientCtxWithSpan context.Context, client *centrifuge.Client, e centrifuge.PublishEvent) (centrifuge.PublishReply, error) { logger.Debug("Client wants to publish", "user", client.UserID(), "client", client.ID(), "channel", e.Channel) - user, ok := livecontext.GetContextSignedUser(clientCtxWithSpan) - if !ok { + user, err := identity.GetRequester(clientCtxWithSpan) + if err != nil { logger.Error("No user found in context", "user", client.UserID(), "client", client.ID(), "channel", e.Channel) return centrifuge.PublishReply{}, centrifuge.ErrorInternal } @@ -1084,7 +1023,7 @@ func (g *GrafanaLive) ClientCount(orgID int64, channel string) (int, error) { } func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.Response { - cmd := dtos.LivePublishCmd{} + cmd := model.LivePublishCmd{} if err := web.Bind(ctx.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } @@ -1123,7 +1062,7 @@ func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.R logger.Error("Error processing input", "user", user, "channel", channel, "error", err) return response.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), nil) } - return response.JSON(http.StatusOK, dtos.LivePublishResponse{}) + return response.JSON(http.StatusOK, model.LivePublishResponse{}) } } @@ -1151,7 +1090,7 @@ func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.R } } logger.Debug("Publication successful", "identity", ctx.GetID(), "channel", cmd.Channel) - return response.JSON(http.StatusOK, dtos.LivePublishResponse{}) + return response.JSON(http.StatusOK, model.LivePublishResponse{}) } type streamChannelListResponse struct { @@ -1178,12 +1117,6 @@ func (g *GrafanaLive) HandleListHTTP(c *contextmodel.ReqContext) response.Respon // HandleInfoHTTP special http response for func (g *GrafanaLive) HandleInfoHTTP(ctx *contextmodel.ReqContext) response.Response { - path := web.Params(ctx.Req)["*"] - if path == "grafana/dashboards/gitops" { - return response.JSON(http.StatusOK, util.DynMap{ - "active": g.GrafanaScope.Dashboards.HasGitOpsObserver(ctx.GetOrgID()), - }) - } return response.JSONStreaming(http.StatusNotFound, util.DynMap{ "message": "Info is not supported for this channel", }) @@ -1405,71 +1338,6 @@ func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *contextmodel.ReqContext) res }) } -// HandleWriteConfigsPutHTTP ... -func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *contextmodel.ReqContext) response.Response { - body, err := io.ReadAll(c.Req.Body) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error reading body", err) - } - var cmd pipeline.WriteConfigUpdateCmd - err = json.Unmarshal(body, &cmd) - if err != nil { - return response.Error(http.StatusBadRequest, "Error decoding write config update command", err) - } - if cmd.UID == "" { - return response.Error(http.StatusBadRequest, "UID required", nil) - } - existingBackend, ok, err := g.pipelineStorage.GetWriteConfig(c.Req.Context(), c.GetOrgID(), pipeline.WriteConfigGetCmd{ - UID: cmd.UID, - }) - if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to get write config", err) - } - if ok { - if cmd.SecureSettings == nil { - cmd.SecureSettings = map[string]string{} - } - secureJSONData, err := g.SecretsService.DecryptJsonData(c.Req.Context(), existingBackend.SecureSettings) - if err != nil { - logger.Error("Error decrypting secure settings", "error", err) - return response.Error(http.StatusInternalServerError, "Error decrypting secure settings", err) - } - for k, v := range secureJSONData { - if _, ok := cmd.SecureSettings[k]; !ok { - cmd.SecureSettings[k] = v - } - } - } - result, err := g.pipelineStorage.UpdateWriteConfig(c.Req.Context(), c.GetOrgID(), cmd) - if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to update write config", err) - } - return response.JSON(http.StatusOK, util.DynMap{ - "writeConfig": pipeline.WriteConfigToDto(result), - }) -} - -// HandleWriteConfigsDeleteHTTP ... -func (g *GrafanaLive) HandleWriteConfigsDeleteHTTP(c *contextmodel.ReqContext) response.Response { - body, err := io.ReadAll(c.Req.Body) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error reading body", err) - } - var cmd pipeline.WriteConfigDeleteCmd - err = json.Unmarshal(body, &cmd) - if err != nil { - return response.Error(http.StatusBadRequest, "Error decoding write config delete command", err) - } - if cmd.UID == "" { - return response.Error(http.StatusBadRequest, "UID required", nil) - } - err = g.pipelineStorage.DeleteWriteConfig(c.Req.Context(), c.GetOrgID(), cmd) - if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to delete write config", err) - } - return response.JSON(http.StatusOK, util.DynMap{}) -} - // Write to the standard log15 logger func handleLog(msg centrifuge.LogEntry) { arr := make([]interface{}, 0) diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index 00ce4bee3f4..9412f32c6f8 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -11,20 +11,16 @@ import ( "testing" "time" + "github.com/centrifugal/centrifuge" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" "github.com/stretchr/testify/require" - "github.com/centrifugal/centrifuge" - "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" @@ -245,7 +241,7 @@ func Test_handleOnPublish_IDTokenExpiration(t *testing.T) { t.Run("expired token", func(t *testing.T) { expiration := time.Now().Add(-time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnPublish(ctx, client, centrifuge.PublishEvent{ Channel: "test", Data: []byte("test"), @@ -257,7 +253,7 @@ func Test_handleOnPublish_IDTokenExpiration(t *testing.T) { t.Run("unexpired token", func(t *testing.T) { expiration := time.Now().Add(time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnPublish(ctx, client, centrifuge.PublishEvent{ Channel: "test", Data: []byte("test"), @@ -280,7 +276,7 @@ func Test_handleOnRPC_IDTokenExpiration(t *testing.T) { t.Run("expired token", func(t *testing.T) { expiration := time.Now().Add(-time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnRPC(ctx, client, centrifuge.RPCEvent{ Method: "grafana.query", Data: []byte("test"), @@ -292,7 +288,7 @@ func Test_handleOnRPC_IDTokenExpiration(t *testing.T) { t.Run("unexpired token", func(t *testing.T) { expiration := time.Now().Add(time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnRPC(ctx, client, centrifuge.RPCEvent{ Method: "grafana.query", Data: []byte("test"), @@ -315,7 +311,7 @@ func Test_handleOnSubscribe_IDTokenExpiration(t *testing.T) { t.Run("expired token", func(t *testing.T) { expiration := time.Now().Add(-time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnSubscribe(ctx, client, centrifuge.SubscribeEvent{ Channel: "test", }) @@ -326,7 +322,7 @@ func Test_handleOnSubscribe_IDTokenExpiration(t *testing.T) { t.Run("unexpired token", func(t *testing.T) { expiration := time.Now().Add(time.Hour) token := createToken(t, &expiration) - ctx := livecontext.SetContextSignedUser(context.Background(), &identity.StaticRequester{IDToken: token}) + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{IDToken: token}) reply, err := g.handleOnSubscribe(ctx, client, centrifuge.SubscribeEvent{ Channel: "test", }) @@ -343,25 +339,26 @@ func setupLiveService(cfg *setting.Cfg, t *testing.T) (*GrafanaLive, error) { cfg = setting.NewCfg() } - return ProvideService(nil, - cfg, + return ProvideService(cfg, routing.NewRouteRegister(), - nil, nil, nil, nil, - db.InitTestDB(t), + nil, nil, nil, nil, &usagestats.UsageStatsMock{T: t}, - nil, featuremgmt.WithFeatures(), - acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), &dashboards.FakeDashboardService{}, - nil, nil) + nil) } type dummyTransport struct { name string } +var ( + _ centrifuge.Transport = (*dummyTransport)(nil) +) + func (t *dummyTransport) Name() string { return t.name } +func (t *dummyTransport) AcceptProtocol() string { return "" } func (t *dummyTransport) Protocol() centrifuge.ProtocolType { return centrifuge.ProtocolTypeJSON } func (t *dummyTransport) ProtocolVersion() centrifuge.ProtocolVersion { return centrifuge.ProtocolVersion2 diff --git a/pkg/services/live/livecontext/context.go b/pkg/services/live/livecontext/context.go index b7c18394cab..3184dc22a93 100644 --- a/pkg/services/live/livecontext/context.go +++ b/pkg/services/live/livecontext/context.go @@ -2,27 +2,8 @@ package livecontext import ( "context" - - "github.com/grafana/grafana/pkg/apimachinery/identity" ) -type signedUserContextKeyType int - -var signedUserContextKey signedUserContextKeyType - -func SetContextSignedUser(ctx context.Context, user identity.Requester) context.Context { - ctx = context.WithValue(ctx, signedUserContextKey, user) - return ctx -} - -func GetContextSignedUser(ctx context.Context) (identity.Requester, bool) { - if val := ctx.Value(signedUserContextKey); val != nil { - user, ok := val.(identity.Requester) - return user, ok - } - return nil, false -} - type streamIDContextKey struct{} func SetContextStreamID(ctx context.Context, streamID string) context.Context { diff --git a/pkg/services/live/model/model.go b/pkg/services/live/model/model.go index 03e7ddcf4eb..1f1c782c914 100644 --- a/pkg/services/live/model/model.go +++ b/pkg/services/live/model/model.go @@ -67,21 +67,9 @@ type ChannelHandlerFactory interface { GetHandlerForPath(path string) (ChannelHandler, error) } -type LiveMessage struct { - ID int64 `xorm:"pk autoincr 'id'"` - OrgID int64 `xorm:"org_id"` - Channel string - Data json.RawMessage - Published time.Time +type LivePublishCmd struct { + Channel string `json:"channel"` + Data json.RawMessage `json:"data,omitempty"` } -type SaveLiveMessageQuery struct { - OrgID int64 `xorm:"org_id"` - Channel string - Data json.RawMessage -} - -type GetLiveMessageQuery struct { - OrgID int64 `xorm:"org_id"` - Channel string -} +type LivePublishResponse struct{} diff --git a/pkg/services/live/pipeline/data_output_builtin.go b/pkg/services/live/pipeline/data_output_builtin.go index 1a4ab62f941..a3c99a9f7d2 100644 --- a/pkg/services/live/pipeline/data_output_builtin.go +++ b/pkg/services/live/pipeline/data_output_builtin.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/services/live/livecontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/live/model" ) @@ -25,8 +25,8 @@ func (s *BuiltinDataOutput) Type() string { } func (s *BuiltinDataOutput) OutputData(ctx context.Context, vars Vars, data []byte) ([]*ChannelData, error) { - u, ok := livecontext.GetContextSignedUser(ctx) - if !ok { + u, err := identity.GetRequester(ctx) + if err != nil { return nil, errors.New("user not found in context") } handler, _, err := s.channelHandlerGetter.GetChannelHandler(ctx, u, vars.Channel) diff --git a/pkg/services/live/pipeline/subscribe_builtin.go b/pkg/services/live/pipeline/subscribe_builtin.go index e9888c59305..20c2660426c 100644 --- a/pkg/services/live/pipeline/subscribe_builtin.go +++ b/pkg/services/live/pipeline/subscribe_builtin.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/model" ) @@ -30,8 +29,8 @@ func (s *BuiltinSubscriber) Type() string { } func (s *BuiltinSubscriber) Subscribe(ctx context.Context, vars Vars, data []byte) (model.SubscribeReply, backend.SubscribeStreamStatus, error) { - u, ok := livecontext.GetContextSignedUser(ctx) - if !ok { + u, err := identity.GetRequester(ctx) + if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil } handler, _, err := s.channelHandlerGetter.GetChannelHandler(ctx, u, vars.Channel) diff --git a/pkg/services/live/pipeline/subscribe_managed_stream.go b/pkg/services/live/pipeline/subscribe_managed_stream.go index 4f668469995..ce26d0d1a30 100644 --- a/pkg/services/live/pipeline/subscribe_managed_stream.go +++ b/pkg/services/live/pipeline/subscribe_managed_stream.go @@ -5,7 +5,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/services/live/livecontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/model" ) @@ -30,8 +30,8 @@ func (s *ManagedStreamSubscriber) Subscribe(ctx context.Context, vars Vars, _ [] logger.Error("Error getting managed stream", "error", err) return model.SubscribeReply{}, 0, err } - u, ok := livecontext.GetContextSignedUser(ctx) - if !ok { + u, err := identity.GetRequester(ctx) + if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil } return stream.OnSubscribe(ctx, u, model.SubscribeEvent{ diff --git a/pkg/services/live/pipeline/subscribe_multiple.go b/pkg/services/live/pipeline/subscribe_multiple.go index 9b7b6789236..a7a6148a99d 100644 --- a/pkg/services/live/pipeline/subscribe_multiple.go +++ b/pkg/services/live/pipeline/subscribe_multiple.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/services/live/model" ) diff --git a/pkg/services/live/pushws/push_pipeline.go b/pkg/services/live/pushws/push_pipeline.go index 0005318bebd..a455e26ba01 100644 --- a/pkg/services/live/pushws/push_pipeline.go +++ b/pkg/services/live/pushws/push_pipeline.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/pipeline" @@ -44,8 +45,8 @@ func (s *PipelinePushHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) return } - user, ok := livecontext.GetContextSignedUser(r.Context()) - if !ok { + user, err := identity.GetRequester(r.Context()) + if err != nil { logger.Error("No user found in context") rw.WriteHeader(http.StatusInternalServerError) return diff --git a/pkg/services/live/pushws/push_stream.go b/pkg/services/live/pushws/push_stream.go index 649a97d9f34..fafcd79f3f0 100644 --- a/pkg/services/live/pushws/push_stream.go +++ b/pkg/services/live/pushws/push_stream.go @@ -5,8 +5,9 @@ import ( "time" "github.com/gorilla/websocket" - liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/managedstream" @@ -47,8 +48,8 @@ func (s *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { return } - user, ok := livecontext.GetContextSignedUser(r.Context()) - if !ok { + user, err := identity.GetRequester(r.Context()) + if err != nil { logger.Error("No user found in context") rw.WriteHeader(http.StatusInternalServerError) return diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index e36c123c621..6754b3c0680 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -173,6 +173,7 @@ var ( Permissions: accesscontrol.ConcatPermissions(templatesReaderRole.Role.Permissions, []accesscontrol.Permission{ {Action: accesscontrol.ActionAlertingNotificationsTemplatesWrite}, {Action: accesscontrol.ActionAlertingNotificationsTemplatesDelete}, + {Action: accesscontrol.ActionAlertingNotificationsTemplatesTest}, }), }, } @@ -288,12 +289,13 @@ var ( Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting:admin", DisplayName: "Full admin access", - Description: "Full write access in Grafana and all external providers, including their permissions and secrets", + Description: "Full write access in Grafana and all external providers, including their permissions, protected fields and secrets", Group: models.AlertRolesGroup, Permissions: accesscontrol.ConcatPermissions(alertingWriterRole.Role.Permissions, []accesscontrol.Permission{ {Action: accesscontrol.ActionAlertingReceiversPermissionsRead, Scope: models.ScopeReceiversAll}, {Action: accesscontrol.ActionAlertingReceiversPermissionsWrite, Scope: models.ScopeReceiversAll}, {Action: accesscontrol.ActionAlertingReceiversReadSecrets, Scope: models.ScopeReceiversAll}, + {Action: accesscontrol.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversAll}, }), }, Grants: []string{string(org.RoleAdmin)}, diff --git a/pkg/services/ngalert/accesscontrol/receivers.go b/pkg/services/ngalert/accesscontrol/receivers.go index eb2904854bf..f12a9114912 100644 --- a/pkg/services/ngalert/accesscontrol/receivers.go +++ b/pkg/services/ngalert/accesscontrol/receivers.go @@ -95,6 +95,26 @@ var ( ) } + // Asserts pre-conditions for access to modify protected fields of receivers. If this evaluates to false, the user cannot modify protected fields of any receivers. + updateReceiversProtectedPreConditionsEval = ac.EvalAll( + updateReceiversPreConditionsEval, + ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected), // Action for receivers. UID scope. + ) + + // Asserts access to modify protected fields of a specific receiver. + updateReceiverProtectedEval = func(uid string) ac.Evaluator { + return ac.EvalAll( + updateReceiverEval(uid), + ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversProvider.GetResourceScopeUID(uid)), + ) + } + + // Asserts access to modify protected fields of all receivers. + updateAllReceiverProtectedEval = ac.EvalAll( + updateAllReceiversEval, + ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversAll), + ) + // Delete // Asserts pre-conditions for delete access to receivers. If this evaluates to false, the user cannot delete any receivers. @@ -141,12 +161,13 @@ var ( ) type ReceiverAccess[T models.Identified] struct { - read actionAccess[T] - readDecrypted actionAccess[T] - create actionAccess[T] - update actionAccess[T] - delete actionAccess[T] - permissions actionAccess[T] + read actionAccess[T] + readDecrypted actionAccess[T] + create actionAccess[T] + update actionAccess[T] + updateProtected actionAccess[T] + delete actionAccess[T] + permissions actionAccess[T] } // NewReceiverAccess creates a new ReceiverAccess service. If includeProvisioningActions is true, the service will include @@ -201,6 +222,18 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision }, authorizeAll: updateAllReceiversEval, }, + updateProtected: actionAccess[T]{ + genericService: genericService{ + ac: a, + }, + resource: "receiver", + action: "update protected fields of", // this produces message "user is not authorized to update protected fields of X receiver" + authorizeSome: updateReceiversProtectedPreConditionsEval, + authorizeOne: func(receiver models.Identified) ac.Evaluator { + return updateReceiverProtectedEval(receiver.GetUID()) + }, + authorizeAll: updateAllReceiverProtectedEval, + }, delete: actionAccess[T]{ genericService: genericService{ ac: a, @@ -311,6 +344,14 @@ func (s ReceiverAccess[T]) AuthorizeUpdate(ctx context.Context, user identity.Re return s.update.Authorize(ctx, user, receiver) } +func (s ReceiverAccess[T]) HasUpdateProtected(ctx context.Context, user identity.Requester, receiver T) (bool, error) { + return s.updateProtected.Has(ctx, user, receiver) +} + +func (s ReceiverAccess[T]) AuthorizeUpdateProtected(ctx context.Context, user identity.Requester, receiver T) error { + return s.updateProtected.Authorize(ctx, user, receiver) +} + // Global // AuthorizeCreate checks if user has access to create receivers. Returns an error if user does not have access. @@ -380,6 +421,12 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester, basePerms.Set(models.ReceiverPermissionDelete, true) // Has access to all receivers. } + if err := s.updateProtected.AuthorizePreConditions(ctx, user); err != nil { + basePerms.Set(models.ReceiverPermissionModifyProtected, false) + } else if err := s.updateProtected.AuthorizeAll(ctx, user); err == nil { + basePerms.Set(models.ReceiverPermissionModifyProtected, true) + } + if basePerms.AllSet() { // Shortcut for the case when all permissions are known based on preconditions. result := make(map[string]models.ReceiverPermissionSet, len(receivers)) @@ -412,6 +459,11 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester, permSet.Set(models.ReceiverPermissionDelete, err == nil) } + if _, ok := permSet.Has(models.ReceiverPermissionModifyProtected); !ok { + err := s.updateProtected.authorize(ctx, user, rcv) + permSet.Set(models.ReceiverPermissionModifyProtected, err == nil) + } + result[rcv.GetUID()] = permSet } return result, nil diff --git a/pkg/services/ngalert/accesscontrol/receivers_test.go b/pkg/services/ngalert/accesscontrol/receivers_test.go index 7be1e9498e2..316fa3a98a6 100644 --- a/pkg/services/ngalert/accesscontrol/receivers_test.go +++ b/pkg/services/ngalert/accesscontrol/receivers_test.go @@ -204,6 +204,33 @@ func TestReceiverAccess(t *testing.T) { recv3.UID: permissions(), }, }, + { + name: "update protected cannot update receivers", + user: newEmptyUser( + ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll}, + ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversAll}, + ), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(), + recv2.UID: permissions(), + recv3.UID: permissions(), + }, + }, + { + name: "update protected receivers", + user: newEmptyUser( + ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll}, + ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, + ), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionModifyProtected), + recv2.UID: permissions(models.ReceiverPermissionWrite), + recv3.UID: permissions(), + }, + }, // Receiver delete. { name: "global receiver delete should have delete but no write", diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index eefbb6dea30..e2b60ad6e39 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -161,7 +161,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { authz: ruleAuthzService, evaluator: api.EvaluatorFactory, cfg: &api.Cfg.UnifiedAlerting, - backtesting: backtesting.NewEngine(api.AppUrl, api.EvaluatorFactory, api.Tracer), + backtesting: backtesting.NewEngine(api.AppUrl, api.EvaluatorFactory, api.Tracer, api.Cfg.UnifiedAlerting, api.FeatureManager), featureManager: api.FeatureManager, appUrl: api.AppUrl, tracer: api.Tracer, diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 90dcb26fc74..f8b7216bcdb 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -10,8 +10,10 @@ import ( "time" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/receivers/schema" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -32,6 +34,7 @@ const ( type receiversAuthz interface { FilterRead(ctx context.Context, user identity.Requester, receivers ...ReceiverStatus) ([]ReceiverStatus, error) + AuthorizeUpdateProtected(context.Context, identity.Requester, ReceiverStatus) error } type AlertmanagerSrv struct { @@ -210,11 +213,16 @@ func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) respons } func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { - if err := srv.crypto.ProcessSecureSettings(c.Req.Context(), c.GetOrgID(), body.Receivers); err != nil { + if err := srv.crypto.ProcessSecureSettings(c.Req.Context(), c.GetOrgID(), body.Receivers, func(receiverName string, paths []schema.IntegrationFieldPath) error { + return srv.receiverAuthz.AuthorizeUpdateProtected(c.Req.Context(), c.SignedInUser, ReceiverStatus{Name: receiverName}) + }); err != nil { var unknownReceiverError UnknownReceiverError if errors.As(err, &unknownReceiverError) { return ErrResp(http.StatusBadRequest, err, "") } + if errors.As(err, &errutil.Error{}) { + return response.Err(err) + } return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration") } diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 588f6dee213..b6849f55dfc 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -251,7 +251,12 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contex logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) provenance := getProvenance(c) - err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, provenance) + filterOpts := &provisioning.FilterOptions{ + NamespaceUIDs: []string{folder.UID}, + RuleGroups: []string{group}, + HasPrometheusRuleDefinition: util.Pointer(true), + } + err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts) if errors.Is(err, models.ErrAlertRuleGroupNotFound) { return response.Empty(http.StatusNotFound) } @@ -370,6 +375,9 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context } datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader)) + if datasourceUID == "" { + datasourceUID = srv.cfg.PrometheusConversion.DefaultDatasourceUID + } if datasourceUID == "" { return response.Err(errDatasourceUIDHeaderMissing) } diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 08ce147a5c3..74e851ed195 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -75,6 +75,46 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { require.Contains(t, string(response.Body()), "Missing datasource UID header") }) + t.Run("without datasource UID header but with config default should succeed", func(t *testing.T) { + srv, _, ruleStore := createConvertPrometheusSrv(t) + // Set the config default + srv.cfg.PrometheusConversion.DefaultDatasourceUID = existingDSUID + + rc := createRequestCtx() + rc.Req.Header.Set(datasourceUIDHeader, "") + + response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup) + + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify that the config default datasource was used + assertRulesUseDatasource(t, ruleStore, existingDSUID, 2) + }) + + t.Run("header should take precedence over config default", func(t *testing.T) { + srv, dsCache, ruleStore := createConvertPrometheusSrv(t) + // Add another datasource + anotherDS := &datasources.DataSource{ + UID: "another-ds", + Type: datasources.DS_PROMETHEUS, + } + dsCache.DataSources = append(dsCache.DataSources, anotherDS) + + // Set the config default to one DS + srv.cfg.PrometheusConversion.DefaultDatasourceUID = "another-ds" + + // But use the header to specify a different one + rc := createRequestCtx() + rc.Req.Header.Set(datasourceUIDHeader, existingDSUID) + + response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup) + + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify that the header datasource was used, not the config default + assertRulesUseDatasource(t, ruleStore, existingDSUID, 2) + }) + t.Run("with invalid datasource should return error", func(t *testing.T) { srv, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() @@ -1390,7 +1430,7 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) { provenanceStore := fakes.NewFakeProvisioningStore() - srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore)) + srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName, withProvenanceStore(provenanceStore)) // Mark the rule as provisioned with API provenance err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus) @@ -1411,6 +1451,37 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { require.Nil(t, remaining) }) }) + + t.Run("should not delete non-imported rule groups", func(t *testing.T) { + folderService := foldertest.NewFakeService() + srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService)) + rc := createRequestCtx() + + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + // Attempt to delete via convert endpoint should return 404 + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusNotFound, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) } func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) { @@ -1730,6 +1801,26 @@ func createRequestCtx() *contextmodel.ReqContext { } } +// assertRulesUseDatasource retrieves all alert rules from the store and verifies they use the expected datasource +func assertRulesUseDatasource(t *testing.T, ruleStore *fakes.RuleStore, expectedDatasourceUID string, expectedRuleCount int) { + t.Helper() + + rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{ + OrgID: 1, + }) + require.NoError(t, err) + require.Len(t, rules, expectedRuleCount) + + for _, rule := range rules { + if rule.Record == nil { + require.NotEmpty(t, rule.Data) + require.Equal(t, expectedDatasourceUID, rule.Data[0].DatasourceUID, rule.Title, expectedDatasourceUID) + } else { + require.Equal(t, expectedDatasourceUID, rule.Record.TargetDatasourceUID) + } + } +} + // Test parseBooleanHeader function which handles boolean header values func TestParseBooleanHeader(t *testing.T) { headerName := "X-Test-Header" diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 25acf56f7d3..dc0f6d1f13d 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -2169,6 +2169,57 @@ func TestRouteGetRuleStatuses(t *testing.T) { }) }) + t.Run("compact mode with receiver_name filter returns only matching rules", func(t *testing.T) { + fakeStore, _, api := setupAPI(t) + + ruleA := gen.With( + gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "folder-1", + RuleGroup: "group-1", + OrgID: orgID, + }), + gen.WithNotificationSettings( + ngmodels.NotificationSettings{ + Receiver: "receiver-a", + GroupBy: []string{"alertname"}, + }, + ), + ).GenerateRef() + fakeStore.PutRule(context.Background(), ruleA) + + ruleB := gen.With( + gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "folder-2", + RuleGroup: "group-2", + OrgID: orgID, + }), + gen.WithNotificationSettings( + ngmodels.NotificationSettings{ + Receiver: "receiver-b", + GroupBy: []string{"alertname"}, + }, + ), + ).GenerateRef() + fakeStore.PutRule(context.Background(), ruleB) + r, err := http.NewRequest("GET", "/api/v1/rules?compact=true&receiver_name=receiver-a", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: r}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + require.Len(t, res.Data.RuleGroups, 1) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Empty(t, res.Data.RuleGroups[0].Rules[0].Query, "Query should be empty in compact mode") + }) + t.Run("provenance as expected", func(t *testing.T) { fakeStore, fakeAIM, api, provStore := setupAPIFull(t) // Rule without provenance @@ -2318,6 +2369,140 @@ func TestRouteGetRuleStatuses(t *testing.T) { } }) + t.Run("multi-page pagination loads provenance correctly", func(t *testing.T) { + fakeStore, fakeAIM, api, fakeProvisioning := setupAPIFull(t) + + // Create 3 groups with 1 rule each: groups 1 and 3 firing, group 2 normal + for i := 1; i <= 3; i++ { + rule := gen.With(gen.WithOrgID(orgID), func(r *ngmodels.AlertRule) { + r.NamespaceUID = "ns-1" + r.RuleGroup = fmt.Sprintf("group-%d", i) + r.UID = fmt.Sprintf("rule-%d", i) + }, withClassicConditionSingleQuery()).GenerateRef() + + alertState := eval.Normal + if i != 2 { + alertState = eval.Alerting + } + fakeAIM.GenerateAlertInstances(orgID, rule.UID, 1, func(s *state.State) *state.State { + s.State = alertState + s.Labels = data.Labels{"test": "label"} + return s + }) + fakeStore.PutRule(context.Background(), rule) + } + + // Set provenance for all rules + err := fakeProvisioning.SetProvenance(context.Background(), + &ngmodels.AlertRule{UID: "rule-1", OrgID: orgID}, orgID, ngmodels.ProvenanceAPI) + require.NoError(t, err) + err = fakeProvisioning.SetProvenance(context.Background(), + &ngmodels.AlertRule{UID: "rule-3", OrgID: orgID}, orgID, ngmodels.ProvenanceFile) + require.NoError(t, err) + + // Request firing groups with group_limit=2 - fetches multiple pages, skipping group 2 + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=2", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 2 firing groups + require.Len(t, res.Data.RuleGroups, 2) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Equal(t, apimodels.Provenance(ngmodels.ProvenanceAPI), res.Data.RuleGroups[0].Rules[0].Provenance) + require.Equal(t, "group-3", res.Data.RuleGroups[1].Name) + require.Equal(t, apimodels.Provenance(ngmodels.ProvenanceFile), res.Data.RuleGroups[1].Rules[0].Provenance) + }) + + t.Run("provenance fetch error returns error response in paginated mode", func(t *testing.T) { + fakeStore, fakeAIM, api, fakeProvisioning := setupAPIFull(t) + + rule := gen.With(gen.WithOrgID(orgID), func(r *ngmodels.AlertRule) { + r.NamespaceUID = "ns-1" + r.RuleGroup = "group-1" + r.UID = "rule-1" + }, withClassicConditionSingleQuery()).GenerateRef() + + fakeAIM.GenerateAlertInstances(orgID, rule.UID, 1, func(s *state.State) *state.State { + s.State = eval.Alerting + s.Labels = data.Labels{"test": "label"} + return s + }) + fakeStore.PutRule(context.Background(), rule) + + fakeProvisioning.GetProvenancesByUIDsFunc = func(ctx context.Context, orgID int64, resourceType string, uids []string) (map[string]ngmodels.Provenance, error) { + return nil, errors.New("database connection failed") + } + + req, err := http.NewRequest("GET", "/api/v1/rules?group_limit=10", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusInternalServerError, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + require.Equal(t, "error", res.Status) + require.Contains(t, res.Error, "failed to load provenance") + }) + + t.Run("provenance fetch error returns error response in non-paginated mode", func(t *testing.T) { + fakeStore, fakeAIM, api, fakeProvisioning := setupAPIFull(t) + + rule := gen.With(gen.WithOrgID(orgID), func(r *ngmodels.AlertRule) { + r.NamespaceUID = "ns-1" + r.RuleGroup = "group-1" + r.UID = "rule-1" + }, withClassicConditionSingleQuery()).GenerateRef() + + fakeAIM.GenerateAlertInstances(orgID, rule.UID, 1, func(s *state.State) *state.State { + s.State = eval.Alerting + s.Labels = data.Labels{"test": "label"} + return s + }) + fakeStore.PutRule(context.Background(), rule) + + fakeProvisioning.GetProvenancesFunc = func(ctx context.Context, orgID int64, resourceType string) (map[string]ngmodels.Provenance, error) { + return nil, errors.New("database connection failed") + } + + req, err := http.NewRequest("GET", "/api/v1/rules", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusInternalServerError, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + require.Equal(t, "error", res.Status) + require.Contains(t, res.Error, "failed to load provenance") + }) + t.Run("state filter continues when first page has no matches", func(t *testing.T) { fakeStore, fakeAIM, api := setupAPI(t) @@ -2886,6 +3071,189 @@ func TestRouteGetRuleStatuses(t *testing.T) { }) } }) + + t.Run("with rule_matcher filter", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "alerting", "severity": "critical"}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "Alerting", "severity": "warning"}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "platform", "severity": "critical"}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule4"), gen.WithLabels(map[string]string{"env": "production"}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule_special"), gen.WithLabels(map[string]string{"key": `value"with"quotes`}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule_empty"), gen.WithLabels(map[string]string{"empty": ""}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule_nonempty"), gen.WithLabels(map[string]string{"empty": "nonempty"}), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule_multiline"), gen.WithLabels(map[string]string{"description": "line1\nline2\\end\"quote"}), gen.WithNoNotificationSettings()) + + testCases := []struct { + name string + matchers []string + expectedUIDs []string + }{ + { + name: "equality matcher filters by team=alerting", + matchers: []string{`{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{"rule1"}, + }, + { + name: "inequality matcher filters severity!=warning", + matchers: []string{`{"name":"severity","value":"warning","isRegex":false,"isEqual":false}`}, + expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"}, + }, + { + name: "regex matcher filters team=~plat.*", + matchers: []string{`{"name":"team","value":"plat.*","isRegex":true,"isEqual":true}`}, + expectedUIDs: []string{"rule3"}, + }, + { + name: "not-regex matcher filters severity!~warn.*", + matchers: []string{`{"name":"severity","value":"warn.*","isRegex":true,"isEqual":false}`}, + expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"}, + }, + { + name: "multiple matchers are ANDed", + matchers: []string{ + `{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`, + `{"name":"severity","value":"critical","isRegex":false,"isEqual":true}`, + }, + expectedUIDs: []string{"rule1"}, + }, + { + name: "matcher with non-existent label returns no rules", + matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{}, + }, + { + name: "equality matcher is case-sensitive", + matchers: []string{`{"name":"team","value":"Alerting","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{"rule2"}, + }, + { + name: "quotes in label value are handled correctly", + matchers: []string{`{"name":"key","value":"value\"with\"quotes","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{"rule_special"}, + }, + { + name: "no matchers returns all rules", + matchers: []string{}, + expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"}, + }, + { + name: "empty string value matches correctly", + matchers: []string{`{"name":"empty","value":"","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_multiline"}, + }, + { + name: "special characters in label value are handled correctly", + matchers: []string{`{"name":"description","value":"line1\nline2\\end\"quote","isRegex":false,"isEqual":true}`}, + expectedUIDs: []string{"rule_multiline"}, + }, + { + name: "inequality matcher on non-existent label matches all rules", + matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":false}`}, + expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reqURL := "/api/v1/rules" + for i, matcher := range tc.matchers { + if i == 0 { + reqURL += "?rule_matcher=" + url.QueryEscape(matcher) + } else { + reqURL += "&rule_matcher=" + url.QueryEscape(matcher) + } + } + + req, err := http.NewRequest("GET", reqURL, nil) + require.NoError(t, err) + ctx := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions}, + } + + resp := api.RouteGetRuleStatuses(ctx) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + require.Equal(t, "success", res.Status) + + actualUIDs := []string{} + for _, group := range res.Data.RuleGroups { + for _, rule := range group.Rules { + actualUIDs = append(actualUIDs, rule.UID) + } + } + + require.ElementsMatch(t, tc.expectedUIDs, actualUIDs) + }) + } + }) + + t.Run("pagination with rule_matcher in-memory filtering", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 3 groups with 2 rules each: + // Group 1 & 2: team=backend (won't match filter) + // Group 3: team=frontend (will match filter) + // This tests that pagination continues fetching when early pages are filtered out + + group1Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace1", RuleGroup: "group1"} + group2Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace2", RuleGroup: "group2"} + group3Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace3", RuleGroup: "group3"} + + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings()) + + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule4"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings()) + + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule5"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithUID("rule6"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings()) + + // Request with regex rule_matcher filter for team=~"alerting" and group_limit=1 to force pagination + matcher := `{"name":"team","value":"alerting","isRegex":true,"isEqual":true}` + reqURL := "/api/v1/rules?rule_matcher=" + url.QueryEscape(matcher) + "&group_limit=1" + + req, err := http.NewRequest("GET", reqURL, nil) + require.NoError(t, err) + ctx := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions}, + } + + resp := api.RouteGetRuleStatuses(ctx) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + require.Equal(t, "success", res.Status) + + actualUIDs := []string{} + for _, group := range res.Data.RuleGroups { + for _, rule := range group.Rules { + actualUIDs = append(actualUIDs, rule.UID) + } + } + + // Should return group3 rules (rule5, rule6), pagination should continue past filtered groups + require.ElementsMatch(t, []string{"rule5", "rule6"}, actualUIDs) + }) } func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index d81f448f600..9fde7a2ec6f 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -412,11 +412,16 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGro deletePermanently = true } - namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), namespaceUID, c.GetOrgID(), c.SignedInUser) + f, err := srv.store.GetNamespaceByUID(c.Req.Context(), namespaceUID, c.GetOrgID(), c.SignedInUser) if err != nil { return toNamespaceErrorResponse(err) } + namespace := ngmodels.NewNamespace(f) + if err := namespace.ValidateForRuleStorage(); err != nil { + return ErrResp(http.StatusBadRequest, fmt.Errorf("%w: %s", ngmodels.ErrAlertRuleFailedValidation, err), "") + } + if err := srv.checkGroupLimits(ruleGroupConfig); err != nil { return ErrResp(http.StatusBadRequest, err, "") } @@ -841,10 +846,14 @@ func (srv RulerSrv) RouteUpdateNamespaceRules(c *contextmodel.ReqContext, body a return ErrResp(http.StatusBadRequest, errors.New("missing request body"), "") } - namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), namespaceUID, c.GetOrgID(), c.SignedInUser) + f, err := srv.store.GetNamespaceByUID(c.Req.Context(), namespaceUID, c.GetOrgID(), c.SignedInUser) if err != nil { return toNamespaceErrorResponse(err) } + namespace := ngmodels.NewNamespace(f) + if err := namespace.ValidateForRuleStorage(); err != nil { + return ErrResp(http.StatusBadRequest, fmt.Errorf("%w: %s", ngmodels.ErrAlertRuleFailedValidation, err), "") + } ruleGroups, _, err := srv.searchAuthorizedAlertRules(c.Req.Context(), authorizedRuleGroupQuery{ User: c.SignedInUser, diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index c6ddeee2a6c..e09088e4669 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -1288,4 +1289,64 @@ func TestRouteUpdateNamespaceRules(t *testing.T) { updatedRules := getRecordedUpdatedRules(ruleStore) require.Empty(t, updatedRules) }) + + t.Run("should reject update when folder is managed by ManagerKindRepo", func(t *testing.T) { + ruleStore := fakes.NewRuleStore(t) + provisioningStore := fakes.NewFakeProvisioningStore() + + // Create a managed folder + managedFolder := randFolder() + managedFolder.ManagedBy = utils.ManagerKindRepo + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], managedFolder) + + // Create some rules in the managed folder + ruleGen := models.RuleGen.With( + models.RuleGen.WithOrgID(orgID), + models.RuleGen.WithNamespaceUID(managedFolder.UID), + ) + rules := ruleGen.GenerateManyRef(2) + ruleStore.PutRule(context.Background(), rules...) + + permissions := createPermissionsForRules(rules, orgID) + requestCtx := createRequestContextWithPerms(orgID, permissions, nil) + + svc := createServiceWithProvenanceStore(ruleStore, provisioningStore) + response := svc.RouteUpdateNamespaceRules(requestCtx, apimodels.UpdateNamespaceRulesRequest{ + IsPaused: util.Pointer(true), + }, managedFolder.UID) + + require.Equal(t, http.StatusBadRequest, response.Status()) + require.Contains(t, string(response.Body()), "cannot store rules in folder managed by Git Sync") + + // Verify no rules were updated + updatedRules := getRecordedUpdatedRules(ruleStore) + require.Empty(t, updatedRules) + }) +} + +func TestRoutePostNameRulesConfig(t *testing.T) { + t.Run("should reject creation when folder is managed by ManagerKindRepo", func(t *testing.T) { + orgID := rand.Int63() + ruleStore := fakes.NewRuleStore(t) + + // Create a managed folder + managedFolder := randFolder() + managedFolder.ManagedBy = utils.ManagerKindRepo + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], managedFolder) + + permissions := map[int64]map[string][]string{ + orgID: { + dashboards.ScopeFoldersProvider.GetResourceScopeUID(managedFolder.UID): {dashboards.ActionFoldersRead}, + }, + } + requestCtx := createRequestContextWithPerms(orgID, permissions, nil) + + svc := createService(ruleStore, nil) + response := svc.RoutePostNameRulesConfig(requestCtx, apimodels.PostableRuleGroupConfig{ + Name: "test-group", + }, managedFolder.UID) + + require.Equal(t, http.StatusBadRequest, response.Status()) + require.Contains(t, string(response.Body()), "cannot store rules in folder managed by Git Sync") + }) } diff --git a/pkg/services/ngalert/api/api_ruler_validation_test.go b/pkg/services/ngalert/api/api_ruler_validation_test.go index 98fbb20cf12..553a02cff30 100644 --- a/pkg/services/ngalert/api/api_ruler_validation_test.go +++ b/pkg/services/ngalert/api/api_ruler_validation_test.go @@ -493,6 +493,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { r.GrafanaManagedAlert.NoDataState = apimodels.OK r.GrafanaManagedAlert.ExecErrState = apimodels.AlertingErrState r.GrafanaManagedAlert.NotificationSettings = &apimodels.AlertRuleNotificationSettings{} + r.GrafanaManagedAlert.MissingSeriesEvalsToResolve = util.Pointer[int64](1) r.For = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() r.KeepFiringFor = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }() return &r @@ -502,6 +503,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { require.Empty(t, alert.NoDataState) require.Empty(t, alert.ExecErrState) require.Nil(t, alert.NotificationSettings) + require.Nil(t, alert.MissingSeriesEvalsToResolve) require.Zero(t, alert.For) require.Zero(t, alert.KeepFiringFor) }, diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index 3bda2e3f28f..13bc1a96c24 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -34,7 +34,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) type folderService interface { @@ -230,54 +229,27 @@ func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimo return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled") } - if cmd.From.After(cmd.To) { - return ErrResp(400, nil, "From cannot be greater than To") - } - - noDataState, err := ngmodels.NoDataStateFromString(string(cmd.NoDataState)) - + rule, err := apivalidation.ValidateBacktestConfig(c.GetOrgID(), cmd, apivalidation.RuleLimitsFromConfig(srv.cfg, srv.featureManager)) if err != nil { - return ErrResp(400, err, "") - } - forInterval := time.Duration(cmd.For) - if forInterval < 0 { - return ErrResp(400, nil, "Bad For interval") + return ErrResp(http.StatusBadRequest, err, "") } - intervalSeconds, err := apivalidation.ValidateInterval(time.Duration(cmd.Interval), srv.cfg.BaseInterval) - if err != nil { - return ErrResp(400, err, "") - } - - queries := AlertQueriesFromApiAlertQueries(cmd.Data) - if err := srv.authz.AuthorizeDatasourceAccessForRule(c.Req.Context(), c.SignedInUser, &ngmodels.AlertRule{Data: queries}); err != nil { + if err := srv.authz.AuthorizeDatasourceAccessForRule(c.Req.Context(), c.SignedInUser, rule); err != nil { return errorToResponse(err) } - rule := &ngmodels.AlertRule{ - // ID: 0, - // Updated: time.Time{}, - // Version: 0, - // NamespaceUID: "", - // DashboardUID: nil, - // PanelID: nil, - // RuleGroup: "", - // RuleGroupIndex: 0, - // ExecErrState: "", - Title: cmd.Title, - // prefix backtesting- is to distinguish between executions of regular rule and backtesting in logs (like expression engine, evaluator, state manager etc) - UID: "backtesting-" + util.GenerateShortUID(), - OrgID: c.GetOrgID(), - Condition: cmd.Condition, - Data: queries, - IntervalSeconds: intervalSeconds, - NoDataState: noDataState, - For: forInterval, - Annotations: cmd.Annotations, - Labels: cmd.Labels, + // Fetch folder path for alert labels, fallback to "Backtesting" if not available + var folderTitle string + if cmd.NamespaceUID != "" { + f, err := srv.folderService.GetNamespaceByUID(c.Req.Context(), cmd.NamespaceUID, c.OrgID, c.SignedInUser) + if err != nil { + srv.log.FromContext(c.Req.Context()).Warn("Failed to fetch folder path for alert labels", "error", err) + } else { + folderTitle = f.Fullpath + } } - result, err := srv.backtesting.Test(c.Req.Context(), c.SignedInUser, rule, cmd.From, cmd.To) + result, err := srv.backtesting.Test(c.Req.Context(), c.SignedInUser, rule, cmd.From, cmd.To, folderTitle) if err != nil { if errors.Is(err, backtesting.ErrInvalidInputData) { return ErrResp(400, err, "Failed to evaluate") @@ -285,9 +257,5 @@ func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimo return ErrResp(500, err, "Failed to evaluate") } - body, err := data.FrameToJSON(result, data.IncludeAll) - if err != nil { - return ErrResp(500, err, "Failed to convert frame to JSON") - } - return response.JSON(http.StatusOK, body) + return response.JSONStreaming(http.StatusOK, result) } diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 46168688ee6..7f8b42bb3a4 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -81,9 +81,15 @@ func (api *API) authorize(method, path string) web.Handler { // additional authorization is done in the request handler eval = ac.EvalPermission(ac.ActionAlertingRuleRead) // Grafana Rules Testing Paths - case http.MethodPost + "/api/v1/rule/backtest": + case http.MethodPost + "/api/v1/rule/backtest": // TODO (yuri) this should be protected by dedicated permission // additional authorization is done in the request handler - eval = ac.EvalPermission(ac.ActionAlertingRuleRead) + eval = ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingRuleRead), + ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingRuleUpdate), + ac.EvalPermission(ac.ActionAlertingRuleCreate), + ), + ) case http.MethodPost + "/api/v1/eval": // additional authorization is done in the request handler eval = ac.EvalPermission(ac.ActionAlertingRuleRead) @@ -251,7 +257,7 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/templates/test": eval = ac.EvalAny( ac.EvalPermission(ac.ActionAlertingNotificationsWrite), - ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead), + ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesTest), ) // External Alertmanager Paths diff --git a/pkg/services/ngalert/api/compat/compat.go b/pkg/services/ngalert/api/compat/compat.go index 5fda13672ba..37c9a8db6ed 100644 --- a/pkg/services/ngalert/api/compat/compat.go +++ b/pkg/services/ngalert/api/compat/compat.go @@ -189,42 +189,11 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE data = append(data, query) } - cPtr := &rule.Condition - if rule.Condition == "" { - cPtr = nil - } - - noDataState := definitions.NoDataState(rule.NoDataState) - ndsPtr := &noDataState - if noDataState == "" { - ndsPtr = nil - } - execErrorState := definitions.ExecutionErrorState(rule.ExecErrState) - eesPtr := &execErrorState - if execErrorState == "" { - eesPtr = nil - } - result := definitions.AlertRuleExport{ - UID: rule.UID, - Title: rule.Title, - For: model.Duration(rule.For), - KeepFiringFor: model.Duration(rule.KeepFiringFor), - Condition: cPtr, - Data: data, - DashboardUID: rule.DashboardUID, - PanelID: rule.PanelID, - NoDataState: ndsPtr, - ExecErrState: eesPtr, - IsPaused: rule.IsPaused, - NotificationSettings: AlertRuleNotificationSettingsExportFromNotificationSettings(rule.NotificationSettings), - Record: AlertRuleRecordExportFromRecord(rule.Record), - } - if rule.For.Seconds() > 0 { - result.ForString = util.Pointer(model.Duration(rule.For).String()) - } - if rule.KeepFiringFor.Seconds() > 0 { - result.KeepFiringForString = util.Pointer(model.Duration(rule.KeepFiringFor).String()) + UID: rule.UID, + Title: rule.Title, + Data: data, + IsPaused: rule.IsPaused, } if rule.Annotations != nil { result.Annotations = &rule.Annotations @@ -232,13 +201,54 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE if rule.Labels != nil { result.Labels = &rule.Labels } - if rule.MissingSeriesEvalsToResolve != nil && *rule.MissingSeriesEvalsToResolve != -1 { - result.MissingSeriesEvalsToResolve = rule.MissingSeriesEvalsToResolve + + if rule.Type() == models.RuleTypeRecording { + populateRecordingRuleExportFields(rule, &result) + } else { + populateAlertingRuleExportFields(rule, &result) } return result, nil } +func populateRecordingRuleExportFields(rule models.AlertRule, result *definitions.AlertRuleExport) { + result.Record = AlertRuleRecordExportFromRecord(rule.Record) +} + +func populateAlertingRuleExportFields(rule models.AlertRule, result *definitions.AlertRuleExport) { + result.DashboardUID = rule.DashboardUID + result.PanelID = rule.PanelID + result.NotificationSettings = AlertRuleNotificationSettingsExportFromNotificationSettings(rule.NotificationSettings) + + if rule.Condition != "" { + result.Condition = &rule.Condition + } + + if rule.NoDataState != "" { + noDataState := definitions.NoDataState(rule.NoDataState) + result.NoDataState = &noDataState + } + + if rule.ExecErrState != "" { + execErrorState := definitions.ExecutionErrorState(rule.ExecErrState) + result.ExecErrState = &execErrorState + } + + result.For = model.Duration(rule.For) + if rule.For > 0 { + result.ForString = util.Pointer(model.Duration(rule.For).String()) + } + + result.KeepFiringFor = model.Duration(rule.KeepFiringFor) + if rule.KeepFiringFor > 0 { + result.KeepFiringForString = util.Pointer(model.Duration(rule.KeepFiringFor).String()) + } + + if rule.MissingSeriesEvalsToResolve != nil && *rule.MissingSeriesEvalsToResolve != -1 { + result.MissingSeriesEvalsToResolve = rule.MissingSeriesEvalsToResolve + } +} + func encodeQueryModel(m map[string]any) (string, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) diff --git a/pkg/services/ngalert/api/compat/compat_test.go b/pkg/services/ngalert/api/compat/compat_test.go index 4a107335945..8b50c609559 100644 --- a/pkg/services/ngalert/api/compat/compat_test.go +++ b/pkg/services/ngalert/api/compat/compat_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/util" ) func TestToModel(t *testing.T) { @@ -115,6 +116,102 @@ func TestToModel(t *testing.T) { }) } +func TestAlertRuleExportFromAlertRule(t *testing.T) { + alertingRule := models.RuleGen.With( + models.RuleGen.WithNotEmptyLabels(2, "lbl-"), + models.RuleGen.WithAnnotations(map[string]string{"ann-key": "ann-value"}), + models.RuleGen.WithFor(2*time.Minute), + models.RuleGen.WithKeepFiringFor(5*time.Minute), + models.RuleGen.WithNotificationSettingsGen(models.NotificationSettingsGen()), + ).Generate() + recordingRule := models.RuleGen.With( + models.RuleGen.WithAllRecordingRules(), + models.RuleGen.WithNotEmptyLabels(2, "lbl-"), + models.RuleGen.WithAnnotations(map[string]string{"ann-key": "ann-value"}), + ).Generate() + + // Build expected exported recording rule + recordingRuleData, err := AlertQueryExportFromAlertQuery(recordingRule.Data[0]) + require.NoError(t, err) + expectedRecordingRuleExport := definitions.AlertRuleExport{ + UID: recordingRule.UID, + Title: recordingRule.Title, + Data: []definitions.AlertQueryExport{recordingRuleData}, + Annotations: &recordingRule.Annotations, + Labels: &recordingRule.Labels, + Record: &definitions.AlertRuleRecordExport{ + Metric: recordingRule.Record.Metric, + From: recordingRule.Record.From, + TargetDatasourceUID: util.Pointer(recordingRule.Record.TargetDatasourceUID), + }, + } + + // Build expected exported alerting rule + alertingRuleData, err := AlertQueryExportFromAlertQuery(alertingRule.Data[0]) + require.NoError(t, err) + noDataState := definitions.NoDataState(alertingRule.NoDataState) + execErrState := definitions.ExecutionErrorState(alertingRule.ExecErrState) + expectedAlertingRuleExport := definitions.AlertRuleExport{ + UID: alertingRule.UID, + Title: alertingRule.Title, + Condition: &alertingRule.Condition, + Data: []definitions.AlertQueryExport{alertingRuleData}, + DashboardUID: alertingRule.DashboardUID, + PanelID: alertingRule.PanelID, + NoDataState: &noDataState, + ExecErrState: &execErrState, + For: prommodel.Duration(alertingRule.For), + KeepFiringFor: prommodel.Duration(alertingRule.KeepFiringFor), + ForString: util.Pointer(prommodel.Duration(alertingRule.For).String()), + KeepFiringForString: util.Pointer(prommodel.Duration(alertingRule.KeepFiringFor).String()), + Annotations: &alertingRule.Annotations, + Labels: &alertingRule.Labels, + NotificationSettings: AlertRuleNotificationSettingsExportFromNotificationSettings(alertingRule.NotificationSettings), + MissingSeriesEvalsToResolve: alertingRule.MissingSeriesEvalsToResolve, + } + + testCases := []struct { + name string + rule models.AlertRule + expected definitions.AlertRuleExport + }{ + { + name: "export recording rule", + rule: recordingRule, + expected: expectedRecordingRuleExport, + }, + { + name: "export alerting rule", + rule: alertingRule, + expected: expectedAlertingRuleExport, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + exported, err := AlertRuleExportFromAlertRule(tc.rule) + require.NoError(t, err) + require.Equal(t, tc.expected, exported) + }) + } +} + +func TestAlertQueryExportFromAlertQuery(t *testing.T) { + query := models.RuleGen.GenerateQuery() + + exported, err := AlertQueryExportFromAlertQuery(query) + require.NoError(t, err) + + require.Equal(t, query.RefID, exported.RefID) + require.Equal(t, query.DatasourceUID, exported.DatasourceUID) + require.Equal(t, int64(time.Duration(query.RelativeTimeRange.From).Seconds()), exported.RelativeTimeRange.FromSeconds) + require.Equal(t, int64(time.Duration(query.RelativeTimeRange.To).Seconds()), exported.RelativeTimeRange.ToSeconds) + require.NotNil(t, exported.QueryType) + require.Equal(t, query.QueryType, *exported.QueryType) + require.NotNil(t, exported.Model) + require.NotEmpty(t, exported.ModelString) +} + func TestAlertRuleMetadataFromModelMetadata(t *testing.T) { t.Run("should convert model metadata to api metadata", func(t *testing.T) { modelMetadata := models.AlertRuleMetadata{ diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index 412a0795469..077761d0caf 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -33,6 +33,12 @@ import ( "go.opentelemetry.io/otel/trace" ) +const ( + queryIncludeInternalLabels = "includeInternalLabels" + queryRuleMatcher = "rule_matcher" + queryInstanceMatcher = "matcher" +) + type RuleStoreReader interface { GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error) ListAlertRulesStoreV2 @@ -48,6 +54,7 @@ type StatusReader interface { type ProvenanceStore interface { GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]ngmodels.Provenance, error) + GetProvenancesByUIDs(ctx context.Context, org int64, resourceType string, uids []string) (map[string]ngmodels.Provenance, error) } type PrometheusSrv struct { @@ -62,6 +69,20 @@ type PrometheusSrv struct { // Package-level OpenTelemetry tracer per Grafana instrumentation conventions. var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus") +// badRequestError returns a Prometheus-compatible error response for bad request data. +func badRequestError(err error) apimodels.RuleResponse { + return apimodels.RuleResponse{ + DiscoveryBase: apimodels.DiscoveryBase{ + Status: "error", + Error: err.Error(), + ErrorType: apiv1.ErrBadData, + }, + Data: apimodels.RuleDiscovery{ + RuleGroups: []apimodels.RuleGroup{}, + }, + } +} + func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv { return &PrometheusSrv{ log, @@ -73,8 +94,6 @@ func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status } } -const queryIncludeInternalLabels = "includeInternalLabels" - func getBoolWithDefault(vals url.Values, field string, d bool) bool { f := vals.Get(field) if f == "" { @@ -188,15 +207,15 @@ func getPanelIDFromQuery(v url.Values) (int64, error) { return 0, nil } -func getMatchersFromQuery(v url.Values) (labels.Matchers, error) { +func getMatchersFromQuery(v url.Values, paramName string) (labels.Matchers, error) { var matchers labels.Matchers - for _, s := range v["matcher"] { + for _, s := range v[paramName] { var m labels.Matcher if err := json.Unmarshal([]byte(s), &m); err != nil { return nil, err } if len(m.Name) == 0 { - return nil, errors.New("bad matcher: the name cannot be blank") + return nil, fmt.Errorf("bad %s: the name cannot be blank", paramName) } matchers = append(matchers, &m) } @@ -296,7 +315,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon allowedNamespaces := map[string]string{} for namespaceUID, folder := range namespaceMap { // only add namespaces that the user has access to rules in - hasAccess, err := srv.authz.HasAccessInFolder(c.Req.Context(), c.SignedInUser, ngmodels.Namespace(*folder.ToFolderReference())) + hasAccess, err := srv.authz.HasAccessInFolder(c.Req.Context(), c.SignedInUser, ngmodels.NewNamespace(folder)) if err != nil { ruleResponse.Status = "error" ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error()) @@ -310,14 +329,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon span.AddEvent("User permissions checked") span.SetAttributes(attribute.Int("allowedNamespaces", len(allowedNamespaces))) - provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.GetOrgID(), (&ngmodels.AlertRule{}).ResourceType()) - if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = fmt.Sprintf("failed to get provenances visible to the user: %s", err.Error()) - ruleResponse.ErrorType = apiv1.ErrServer - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - ruleResponse = PrepareRuleGroupStatusesV2( srv.log, srv.store, @@ -329,7 +340,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon }, RuleStatusMutatorGenerator(srv.status), RuleAlertStateMutatorGenerator(srv.manager), - provenanceRecords, + srv.provenanceStore, ) return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) @@ -339,7 +350,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon type RuleStatusMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule) // mutator function used to attach alert states to the rule and returns the totals and filtered totals -type RuleAlertStateMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (total map[string]int64, filteredTotal map[string]int64) +type RuleAlertStateMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, limitAlerts int64) (total map[string]int64, filteredTotal map[string]int64) func RuleStatusMutatorGenerator(statusReader StatusReader) RuleStatusMutator { return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule) { @@ -359,32 +370,18 @@ func RuleStatusMutatorGenerator(statusReader StatusReader) RuleStatusMutator { } func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAlertStateMutator { - return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (map[string]int64, map[string]int64) { + return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, limitAlerts int64) (map[string]int64, map[string]int64) { states := manager.GetStatesForRuleUID(source.OrgID, source.UID) totals := make(map[string]int64) totalsFiltered := make(map[string]int64) for _, alertState := range states { activeAt := alertState.StartsAt - valString := "" - if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering { - valString = FormatValues(alertState) - } stateKey := strings.ToLower(alertState.State.String()) totals[stateKey] += 1 // Do not add error twice when execution error state is Error if alertState.Error != nil && source.ExecErrState != ngmodels.ErrorErrState { totals["error"] += 1 } - alert := apimodels.Alert{ - Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)), - Annotations: apimodels.LabelsFromMap(alertState.Annotations), - - // TODO: or should we make this two fields? Using one field lets the - // frontend use the same logic for parsing text on annotations and this. - State: state.FormatStateAndReason(alertState.State, alertState.StateReason), - ActiveAt: &activeAt, - Value: valString, - } // Set the state of the rule based on the state of its alerts. // Only update the rule state with 'pending' or 'recovering' if the current state is 'inactive'. @@ -424,7 +421,23 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler totalsFiltered["error"] += 1 } - toMutate.Alerts = append(toMutate.Alerts, alert) + if limitAlerts != 0 { + valString := "" + if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering { + valString = FormatValues(alertState) + } + + toMutate.Alerts = append(toMutate.Alerts, apimodels.Alert{ + Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)), + Annotations: apimodels.LabelsFromMap(alertState.Annotations), + + // TODO: or should we make this two fields? Using one field lets the + // frontend use the same logic for parsing text on annotations and this. + State: state.FormatStateAndReason(alertState.State, alertState.StateReason), + ActiveAt: &activeAt, + Value: valString, + }) + } } return totals, totalsFiltered } @@ -434,6 +447,7 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler type paginationContext struct { opts RuleGroupStatusesOptions provenanceRecords map[string]ngmodels.Provenance + provenanceStore ProvenanceStore ruleStatusMutator RuleStatusMutator alertStateMutator RuleAlertStateMutator @@ -454,9 +468,11 @@ type paginationContext struct { stateFilterSet map[eval.State]struct{} healthFilterSet map[string]struct{} matchers labels.Matchers + ruleLabelMatchers labels.Matchers labelOptions []ngmodels.LabelOption limitAlertsPerRule int64 limitRulesPerGroup int64 + compact bool } // pageResult is the result of fetching and filtering of one page @@ -475,6 +491,9 @@ func accumulateTotals(dest, source map[string]int64) { // fetchAndFilterPage fetches one page from the store and applies filters func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlertRulesStoreV2, span trace.Span, token string, remainingGroups, remainingRules int64) (pageResult, error) { + // Split matchers: only equality/inequality are supported by the store + storeMatchers := filterOutRegexMatchers(ctx.ruleLabelMatchers) + byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ OrgID: ctx.opts.OrgID, @@ -487,11 +506,13 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert DataSourceUIDs: ctx.dataSourceUIDs, SearchTitle: ctx.title, SearchRuleGroup: ctx.searchRuleGroup, + LabelMatchers: storeMatchers, }, RuleType: ctx.ruleType, Limit: remainingGroups, RuleLimit: remainingRules, ContinueToken: token, + Compact: ctx.compact, } ruleList, newToken, err := store.ListAlertRulesByGroup(ctx.opts.Ctx, &byGroupQuery) @@ -505,6 +526,37 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert ) span.AddEvent("Alert rules retrieved from store") + // Load provenance for this page's rules + if ctx.provenanceStore != nil { + maxGroups := getInt64WithDefault(ctx.opts.Query, "group_limit", -1) + maxRules := getInt64WithDefault(ctx.opts.Query, "rule_limit", -1) + + if maxGroups > 0 || maxRules > 0 { + // Paginated, fetch and merge provenances for this page + uids := make([]string, 0, len(ruleList)) + for _, rule := range ruleList { + uids = append(uids, rule.UID) + } + pageProvenances, err := ctx.provenanceStore.GetProvenancesByUIDs(ctx.opts.Ctx, ctx.opts.OrgID, (&ngmodels.AlertRule{}).ResourceType(), uids) + if err != nil { + return pageResult{}, fmt.Errorf("failed to load provenance: %w", err) + } + if ctx.provenanceRecords == nil { + ctx.provenanceRecords = pageProvenances + } else { + maps.Copy(ctx.provenanceRecords, pageProvenances) + } + } else if ctx.provenanceRecords == nil { + // Not paginated, fetch all once + var err error + ctx.provenanceRecords, err = ctx.provenanceStore.GetProvenances(ctx.opts.Ctx, ctx.opts.OrgID, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return pageResult{}, fmt.Errorf("failed to load provenance: %w", err) + } + } + } + span.AddEvent("Provenances retrieved from store") + groupedRules := getGroupedRules(log, ruleList, ctx.ruleNamesSet, ctx.opts.AllowedNamespaces) result := pageResult{ @@ -519,7 +571,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert log, rg.GroupKey, rg.Folder, rg.Rules, ctx.provenanceRecords, ctx.limitAlertsPerRule, ctx.stateFilterSet, ctx.matchers, ctx.labelOptions, - ctx.ruleStatusMutator, ctx.alertStateMutator, + ctx.ruleStatusMutator, ctx.alertStateMutator, ctx.compact, ) ruleGroup.Totals = totals accumulateTotals(result.totalsDelta, totals) @@ -532,6 +584,8 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert filterRulesByHealth(ruleGroup, ctx.healthFilterSet) } + filterRulesByLabelMatchers(ruleGroup, ctx.ruleLabelMatchers) + if ctx.limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > ctx.limitRulesPerGroup { ruleGroup.Rules = ruleGroup.Rules[0:ctx.limitRulesPerGroup] } @@ -544,6 +598,17 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert return result, nil } +func filterOutRegexMatchers(matchers labels.Matchers) labels.Matchers { + var result labels.Matchers + for _, m := range matchers { + if m.Type == labels.MatchEqual || m.Type == labels.MatchNotEqual { + result = append(result, m) + } + } + + return result +} + // paginateRuleGroups fetches pages until limits are satisfied applying filters at each step func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *paginationContext, span trace.Span, maxGroups, maxRules int64, startToken string) ([]apimodels.RuleGroup, map[string]int64, string, error) { allGroups := []apimodels.RuleGroup{} @@ -603,7 +668,7 @@ func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *pagina return allGroups, rulesTotals, continueToken, nil } -func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse { +func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceStore ProvenanceStore) apimodels.RuleResponse { ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2") defer span.End() opts.Ctx = ctx @@ -642,21 +707,30 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt attribute.Int64("limit_rules", limitRulesPerGroup), attribute.Int64("limit_alerts", limitAlertsPerRule), ) - matchers, err := getMatchersFromQuery(opts.Query) + matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) } span.SetAttributes(attribute.Int("matcher_count", len(matchers))) + ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher) + if err != nil { + return badRequestError(err) + } + regexCount := 0 + for _, m := range ruleLabelMatchers { + if m.Type == labels.MatchRegexp || m.Type == labels.MatchNotRegexp { + regexCount++ + } + } + span.SetAttributes( + attribute.Int("rule_matcher_count", len(ruleLabelMatchers)), + attribute.Int("rule_matcher_regex_count", regexCount), + ) + stateFilterSet, err := GetStatesFromQuery(opts.Query) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) } span.SetAttributes( attribute.Int("state_filter_count", len(stateFilterSet)), @@ -665,10 +739,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt healthFilterSet, err := GetHealthFromQuery(opts.Query) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) } span.SetAttributes( attribute.Int("health_filter_count", len(healthFilterSet)), @@ -785,9 +856,12 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt } span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet))) + compact := getBoolWithDefault(opts.Query, "compact", false) + span.SetAttributes(attribute.Bool("compact", compact)) pagCtx := &paginationContext{ opts: opts, - provenanceRecords: provenanceRecords, + provenanceRecords: nil, + provenanceStore: provenanceStore, ruleStatusMutator: ruleStatusMutator, alertStateMutator: alertStateMutator, namespaceUIDs: namespaceUIDs, @@ -804,9 +878,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt stateFilterSet: stateFilterSet, healthFilterSet: healthFilterSet, matchers: matchers, + ruleLabelMatchers: ruleLabelMatchers, labelOptions: labelOptions, limitAlertsPerRule: limitAlertsPerRule, limitRulesPerGroup: limitRulesPerGroup, + compact: compact, } groups, rulesTotals, continueToken, err := paginateRuleGroups(log, store, pagCtx, span, maxGroups, maxRules, nextToken) @@ -828,6 +904,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt return ruleResponse } +// nolint:gocyclo func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse { ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ @@ -841,41 +918,30 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru dashboardUID := opts.Query.Get("dashboard_uid") panelID, err := getPanelIDFromQuery(opts.Query) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = fmt.Sprintf("invalid panel_id: %s", err.Error()) - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(fmt.Errorf("invalid panel_id: %w", err)) } if dashboardUID == "" && panelID != 0 { - ruleResponse.Status = "error" - ruleResponse.Error = "panel_id must be set with dashboard_uid" - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(errors.New("panel_id must be set with dashboard_uid")) } limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1) limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1) - matchers, err := getMatchersFromQuery(opts.Query) + matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) + } + ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher) + if err != nil { + return badRequestError(err) } stateFilterSet, err := GetStatesFromQuery(opts.Query) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) } healthFilterSet, err := GetHealthFromQuery(opts.Query) if err != nil { - ruleResponse.Status = "error" - ruleResponse.Error = err.Error() - ruleResponse.ErrorType = apiv1.ErrBadData - return ruleResponse + return badRequestError(err) } var labelOptions []ngmodels.LabelOption @@ -908,6 +974,9 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru dataSourceUIDs := opts.Query["datasource_uid"] searchRuleGroup := opts.Query.Get("search.rule_group") + // Split matchers: only equality/inequality are supported by the store + storeMatchers := filterOutRegexMatchers(ruleLabelMatchers) + alertRuleQuery := ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, @@ -919,6 +988,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru SearchTitle: title, SearchRuleGroup: searchRuleGroup, DataSourceUIDs: dataSourceUIDs, + LabelMatchers: storeMatchers, } ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery) if err != nil { @@ -959,7 +1029,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru break } - ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator) + ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator, false) ruleGroup.Totals = totals for k, v := range totals { rulesTotals[k] += v @@ -973,6 +1043,10 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru filterRulesByHealth(ruleGroup, healthFilterSet) } + if len(ruleLabelMatchers) > 0 { + filterRulesByLabelMatchers(ruleGroup, ruleLabelMatchers) + } + if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup { ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup] } @@ -1100,6 +1174,30 @@ func filterRulesByHealth(ruleGroup *apimodels.RuleGroup, withHealthFast map[stri ruleGroup.Rules = filteredRules } +func filterRulesByLabelMatchers(ruleGroup *apimodels.RuleGroup, matchers labels.Matchers) { + if len(matchers) == 0 { + return + } + + filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules)) + + for _, rule := range ruleGroup.Rules { + ruleLabels := rule.Labels.Map() + matches := true + for _, m := range matchers { + if !m.Matches(ruleLabels[m.Name]) { + matches = false + break + } + } + if matches { + filteredRules = append(filteredRules, rule) + } + } + + ruleGroup.Rules = filteredRules +} + // This is the same as matchers.Matches but avoids the need to create a LabelSet func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { for _, m := range matchers { @@ -1110,7 +1208,7 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { return true } -func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator) (*apimodels.RuleGroup, map[string]int64) { +func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator, compact bool) (*apimodels.RuleGroup, map[string]int64) { newGroup := &apimodels.RuleGroup{ Name: groupKey.RuleGroup, // file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. @@ -1126,10 +1224,14 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull if prov, exists := provenanceRecords[rule.ResourceID()]; exists { provenance = prov } + var query string + if !compact { + query = ruleToQuery(log, rule) + } alertingRule := apimodels.AlertingRule{ State: "inactive", Name: rule.Title, - Query: ruleToQuery(log, rule), + Query: query, QueriedDatasourceUIDs: extractDatasourceUIDs(rule), Duration: rule.For.Seconds(), KeepFiringFor: rule.KeepFiringFor.Seconds(), @@ -1153,7 +1255,7 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull } // mutate rule for alert states - totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions) + totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions, limitAlerts) if alertingRule.State != "" { rulesTotals[alertingRule.State] += 1 diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl index 3a5882e0a49..5daa4dce406 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl @@ -173,4 +173,49 @@ resource "grafana_rule_group" "rule_group_d3e8424bfbf66bc3" { from = "condition" } } + rule { + name = "recording rule with target" + + data { + ref_id = "query" + + relative_time_range { + from = 18000 + to = 10800 + } + + datasource_uid = "000000002" + model = "{\"expr\":\"rate(http_requests_total[5m])\",\"hide\":false,\"interval\":\"\",\"intervalMs\":1000,\"legendFormat\":\"\",\"maxDataPoints\":100,\"refId\":\"query\"}" + } + data { + ref_id = "reduced" + + relative_time_range { + from = 18000 + to = 10800 + } + + datasource_uid = "__expr__" + model = "{\"expression\":\"query\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"reducer\":\"mean\",\"refId\":\"reduced\",\"type\":\"reduce\"}" + } + data { + ref_id = "condition" + + relative_time_range { + from = 18000 + to = 10800 + } + + datasource_uid = "__expr__" + model = "{\"expression\":\"$reduced > 5\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"refId\":\"condition\",\"type\":\"math\"}" + } + + is_paused = false + + record { + metric = "http_requests_rate" + from = "condition" + target_datasource_uid = "000000003" + } + } } diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.json b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.json index bde4bb31f57..4fe9a2dc75a 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.json +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.json @@ -233,6 +233,67 @@ "metric": "test_metric", "from": "condition" } + }, + { + "title": "recording rule with target", + "data": [ + { + "refId": "query", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "000000002", + "model": { + "expr": "rate(http_requests_total[5m])", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "$reduced \u003e 5", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "isPaused": false, + "record": { + "metric": "http_requests_rate", + "from": "condition", + "targetDatasourceUid": "000000003" + } } ] } diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.yaml b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.yaml index bdb6e9e8362..242a2204823 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.yaml +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.yaml @@ -187,3 +187,48 @@ groups: record: metric: test_metric from: condition + - title: recording rule with target + data: + - refId: query + relativeTimeRange: + from: 18000 + to: 10800 + datasourceUid: "000000002" + model: + expr: rate(http_requests_total[5m]) + hide: false + interval: "" + intervalMs: 1000 + legendFormat: "" + maxDataPoints: 100 + refId: query + - refId: reduced + relativeTimeRange: + from: 18000 + to: 10800 + datasourceUid: __expr__ + model: + expression: query + hide: false + intervalMs: 1000 + maxDataPoints: 100 + reducer: mean + refId: reduced + type: reduce + - refId: condition + relativeTimeRange: + from: 18000 + to: 10800 + datasourceUid: __expr__ + model: + expression: $reduced > 5 + hide: false + intervalMs: 1000 + maxDataPoints: 100 + refId: condition + type: math + isPaused: false + record: + metric: http_requests_rate + from: condition + targetDatasourceUid: "000000003" diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101.json b/pkg/services/ngalert/api/test-data/post-rulegroup-101.json index f0eaa36d40d..e9936041979 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101.json +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101.json @@ -240,6 +240,71 @@ "from": "condition" } } + }, + { + "grafana_alert": { + "title": "recording rule with target", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "000000002", + "model": { + "expr": "rate(http_requests_total[5m])", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "$reduced > 5", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "record": { + "metric": "http_requests_rate", + "from": "condition", + "target_datasource_uid": "000000003" + } + } } ] } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index c3fce7cb1b4..c0bc3685638 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -5,7 +5,8 @@ import ( "regexp" "strings" - "github.com/grafana/alerting/templates" + "github.com/grafana/alerting/definition" + "github.com/grafana/alerting/notify" "go.yaml.in/yaml/v3" ) @@ -31,11 +32,18 @@ func (t *NotificationTemplate) Validate() error { content = fmt.Sprintf("{{ define \"%s\" }}\n%s\n{{ end }}", t.Name, content) } t.Template = content - def := templates.TemplateDefinition{ - Name: t.Name, - Template: t.Template, - Kind: templates.GrafanaKind, + if t.Kind == "" { + t.Kind = definition.GrafanaTemplateKind } + postable := definition.PostableApiTemplate{ + Name: t.Name, + Content: t.Template, + Kind: t.Kind, + } + if err := postable.Validate(); err != nil { + return err + } + def := notify.PostableAPITemplateToTemplateDefinition(postable) return def.Validate() } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go index 74f186003d7..2b5139a4960 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go @@ -462,6 +462,16 @@ func TestValidateNotificationTemplates(t *testing.T) { }, expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, }, + { + name: "unknown template kind", + template: NotificationTemplate{ + Name: "Alert Instance Template", + Template: `{{ define "Same name as definition" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}`, + Provenance: "test", + Kind: "unknown", + }, + expError: errors.New("unknown template kind: unknown"), + }, } for _, tt := range tc { diff --git a/pkg/services/ngalert/api/tooling/definitions/prom.go b/pkg/services/ngalert/api/tooling/definitions/prom.go index 128c69787c0..05fb62dc283 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom.go @@ -462,4 +462,10 @@ type GetGrafanaRuleStatusesParams struct { // in: query // required: false Matchers []string `json:"matcher"` + + // Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {"type":0,"name":"severity","value":"critical"}). + // For equality matchers with empty string values (e.g., name=""), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior). + // in: query + // required: false + RuleLabelMatchers []string `json:"rule_matcher"` } diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go index 1633a903822..3c64573a812 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go @@ -1,5 +1,7 @@ package definitions +import "github.com/grafana/alerting/definition" + // swagger:route GET /v1/provisioning/templates provisioning stable RouteGetTemplates // // Get all notification template groups. @@ -55,11 +57,12 @@ type RouteDeleteTemplateParam struct { // swagger:model type NotificationTemplate struct { - UID string `json:"-" yaml:"-"` - Name string `json:"name"` - Template string `json:"template"` - Provenance Provenance `json:"provenance,omitempty"` - ResourceVersion string `json:"version,omitempty"` + UID string `json:"-" yaml:"-"` + Name string `json:"name"` + Template string `json:"template"` + Provenance Provenance `json:"provenance,omitempty"` + ResourceVersion string `json:"version,omitempty"` + Kind definition.TemplateKind `json:"-" yaml:"-"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/definitions/testing.go b/pkg/services/ngalert/api/tooling/definitions/testing.go index 2c228e94758..e094c8515b3 100644 --- a/pkg/services/ngalert/api/tooling/definitions/testing.go +++ b/pkg/services/ngalert/api/tooling/definitions/testing.go @@ -221,15 +221,21 @@ type BacktestConfig struct { To time.Time `json:"to"` Interval model.Duration `json:"interval,omitempty"` - Condition string `json:"condition"` - Data []AlertQuery `json:"data"` - For model.Duration `json:"for,omitempty"` + Condition string `json:"condition"` + Data []AlertQuery `json:"data"` + For *model.Duration `json:"for,omitempty"` + KeepFiringFor *model.Duration `json:"keep_firing_for,omitempty"` - Title string `json:"title"` - Labels map[string]string `json:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` + Title string `json:"title"` + Labels map[string]string `json:"labels,omitempty"` - NoDataState NoDataState `json:"no_data_state"` + NoDataState NoDataState `json:"no_data_state"` + ExecErrState ExecutionErrorState `json:"exec_err_state"` + MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty"` + + UID string `json:"uid,omitempty"` + RuleGroup string `json:"rule_group,omitempty"` + NamespaceUID string `json:"namespace_uid,omitempty"` } // swagger:model diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index d9d38ce03b0..1243007dfcf 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -7561,6 +7561,15 @@ }, "name": "matcher", "type": "array" + }, + { + "description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).", + "in": "query", + "items": { + "type": "string" + }, + "name": "rule_matcher", + "type": "array" } ], "responses": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index adb65e92ad9..81e0aa894c9 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1947,6 +1947,15 @@ "description": "Filter by label matchers encoded as JSON representations of Prometheus matchers (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}). Provide one matcher per query string value.", "name": "matcher", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).", + "name": "rule_matcher", + "in": "query" } ], "responses": { diff --git a/pkg/services/ngalert/api/validation/api_ruler_validation.go b/pkg/services/ngalert/api/validation/api_ruler_validation.go index c2baf8108ba..09e601a4711 100644 --- a/pkg/services/ngalert/api/validation/api_ruler_validation.go +++ b/pkg/services/ngalert/api/validation/api_ruler_validation.go @@ -193,6 +193,7 @@ func validateRecordingRuleFields(in *apimodels.PostableExtendedRuleNode, newRule newRule.For = 0 newRule.KeepFiringFor = 0 newRule.NotificationSettings = nil + newRule.MissingSeriesEvalsToResolve = nil return newRule, nil } @@ -248,6 +249,21 @@ func ValidateCondition(condition string, queries []apimodels.AlertQuery, canPatc return nil } +func validateGroupInterval(incoming prommodels.Duration, limits RuleLimits) (time.Duration, error) { + interval := time.Duration(incoming) + if interval == 0 { + // if group interval is 0 (undefined) then we automatically fall back to the default interval + interval = limits.DefaultRuleEvaluationInterval + } + + if interval < 0 || int64(interval.Seconds())%int64(limits.BaseInterval.Seconds()) != 0 { + return 0, fmt.Errorf("rule evaluation interval (%d second) should be positive number that is multiple of the base interval of %d seconds", int64(interval.Seconds()), int64(limits.BaseInterval.Seconds())) + } + + // TODO should we validate that interval is >= cfg.MinInterval? Currently, we allow to save but fix the specified interval if it is < cfg.MinInterval + return interval, nil +} + func ValidateInterval(interval, baseInterval time.Duration) (int64, error) { intervalSeconds := int64(interval.Seconds()) @@ -335,18 +351,11 @@ func ValidateRuleGroup( return nil, fmt.Errorf("rule group name is too long. Max length is %d", store.AlertRuleMaxRuleGroupNameLength) } - interval := time.Duration(ruleGroupConfig.Interval) - if interval == 0 { - // if group interval is 0 (undefined) then we automatically fall back to the default interval - interval = limits.DefaultRuleEvaluationInterval + interval, err := validateGroupInterval(ruleGroupConfig.Interval, limits) + if err != nil { + return nil, err } - if interval < 0 || int64(interval.Seconds())%int64(limits.BaseInterval.Seconds()) != 0 { - return nil, fmt.Errorf("rule evaluation interval (%d second) should be positive number that is multiple of the base interval of %d seconds", int64(interval.Seconds()), int64(limits.BaseInterval.Seconds())) - } - - // TODO should we validate that interval is >= cfg.MinInterval? Currently, we allow to save but fix the specified interval if it is < cfg.MinInterval - // If the rule group is reserved for no-group rules, we cannot have multiple rules in it. if isNoGroupRuleGroup && len(ruleGroupConfig.Rules) > 1 { return nil, fmt.Errorf("rule group %s is reserved for no-group rules and cannot be used for rule groups with multiple rules", ruleGroupConfig.Name) @@ -409,3 +418,32 @@ func ValidateNotificationSettings(n *apimodels.AlertRuleNotificationSettings) ([ s, }, nil } + +func ValidateBacktestConfig(orgId int64, config apimodels.BacktestConfig, limits RuleLimits) (*ngmodels.AlertRule, error) { + if config.From.After(config.To) { + return nil, fmt.Errorf("invalid testing range: from %s must be before to %s", config.From, config.To) + } + + interval, err := validateGroupInterval(config.Interval, limits) + if err != nil { + return nil, err + } + + return ValidateRuleNode(&apimodels.PostableExtendedRuleNode{ + ApiRuleNode: &apimodels.ApiRuleNode{ + For: config.For, + KeepFiringFor: config.KeepFiringFor, + Labels: config.Labels, + Annotations: nil, + }, + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + Title: config.Title, + Condition: config.Condition, + Data: config.Data, + UID: config.UID, + NoDataState: config.NoDataState, + ExecErrState: config.ExecErrState, + MissingSeriesEvalsToResolve: config.MissingSeriesEvalsToResolve, + }, + }, config.RuleGroup, interval, orgId, config.NamespaceUID, limits) +} diff --git a/pkg/services/ngalert/backtesting/engine.go b/pkg/services/ngalert/backtesting/engine.go index 31c968eb234..b4a534fe134 100644 --- a/pkg/services/ngalert/backtesting/engine.go +++ b/pkg/services/ngalert/backtesting/engine.go @@ -15,10 +15,16 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/schedule" + "github.com/grafana/grafana/pkg/services/ngalert/schedule/ticker" "github.com/grafana/grafana/pkg/services/ngalert/state" + "github.com/grafana/grafana/pkg/services/ngalert/state/historian" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) var ( @@ -28,7 +34,7 @@ var ( backtestingEvaluatorFactory = newBacktestingEvaluator ) -type callbackFunc = func(evaluationIndex int, now time.Time, results eval.Results) error +type callbackFunc = func(evaluationIndex int, now time.Time, results eval.Results) (bool, error) type backtestingEvaluator interface { Eval(ctx context.Context, from time.Time, interval time.Duration, evaluations int, callback callbackFunc) error @@ -40,11 +46,17 @@ type stateManager interface { } type Engine struct { - evalFactory eval.EvaluatorFactory - createStateManager func() stateManager + evalFactory eval.EvaluatorFactory + createStateManager func() stateManager + disableGrafanaFolder bool + featureToggles featuremgmt.FeatureToggles + minInterval time.Duration + baseInterval time.Duration + jitterStrategy schedule.JitterStrategy + maxEvaluations int } -func NewEngine(appUrl *url.URL, evalFactory eval.EvaluatorFactory, tracer tracing.Tracer) *Engine { +func NewEngine(appUrl *url.URL, evalFactory eval.EvaluatorFactory, tracer tracing.Tracer, cfg setting.UnifiedAlertingSettings, toggles featuremgmt.FeatureToggles) *Engine { return &Engine{ evalFactory: evalFactory, createStateManager: func() stateManager { @@ -60,74 +72,139 @@ func NewEngine(appUrl *url.URL, evalFactory eval.EvaluatorFactory, tracer tracin } return state.NewManager(cfg, state.NewNoopPersister()) }, + disableGrafanaFolder: false, + featureToggles: toggles, + minInterval: cfg.MinInterval, + baseInterval: cfg.BaseInterval, + maxEvaluations: cfg.BacktestingMaxEvaluations, + jitterStrategy: schedule.JitterStrategyFrom(cfg, toggles), } } -func (e *Engine) Test(ctx context.Context, user identity.Requester, rule *models.AlertRule, from, to time.Time) (*data.Frame, error) { - ruleCtx := models.WithRuleKey(ctx, rule.GetKey()) - logger := logger.FromContext(ctx) - +func (e *Engine) Test(ctx context.Context, user identity.Requester, rule *models.AlertRule, from, to time.Time, folderTitle string) (res *data.Frame, err error) { + if rule == nil { + return nil, fmt.Errorf("%w: rule is not defined", ErrInvalidInputData) + } if !from.Before(to) { - return nil, fmt.Errorf("%w: invalid interval of the backtesting [%d,%d]", ErrInvalidInputData, from.Unix(), to.Unix()) + return nil, fmt.Errorf("%w: invalid interval [%d,%d]", ErrInvalidInputData, from.Unix(), to.Unix()) } - if to.Sub(from).Seconds() < float64(rule.IntervalSeconds) { - return nil, fmt.Errorf("%w: interval of the backtesting [%d,%d] is less than evaluation interval [%ds]", ErrInvalidInputData, from.Unix(), to.Unix(), rule.IntervalSeconds) + + ruleCtx := models.WithRuleKey(ctx, rule.GetKey()) + logger := logger.FromContext(ruleCtx).New("backtesting", util.GenerateShortUID()) + + var warns []string + if rule.GetInterval() < e.minInterval { + logger.Warn("Interval adjusted to minimal interval", "originalInterval", rule.GetInterval(), "adjustedInterval", e.minInterval) + rule = rule.Copy() + rule.IntervalSeconds = int64(e.minInterval.Seconds()) + warns = append(warns, fmt.Sprintf("Interval adjusted to minimal interval %ds", rule.IntervalSeconds)) } - length := int(to.Sub(from).Seconds()) / int(rule.IntervalSeconds) - stateManager := e.createStateManager() + effectiveStrategy := e.jitterStrategy + if e.jitterStrategy == schedule.JitterByGroup && (rule.RuleGroup == "" || rule.NamespaceUID == "") || + e.jitterStrategy == schedule.JitterByRule && rule.UID == "" { + logger.Warn(fmt.Sprintf("Jitter strategy is set to %s, but rule group or namespace is not set. Ignore jitter", e.jitterStrategy)) + warns = append(warns, fmt.Sprintf("Jitter strategy is set to %s, but rule group or namespace is not set. Ignore jitter. The results of testing will be different than real evaluations", e.jitterStrategy)) + effectiveStrategy = schedule.JitterNever + } + jitterOffset := schedule.JitterOffsetInDuration(rule, e.baseInterval, effectiveStrategy) + firstEval, err := getFirstEvaluationTime(from, rule, e.baseInterval, jitterOffset) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrInvalidInputData, err) + } - evaluator, err := backtestingEvaluatorFactory(ruleCtx, e.evalFactory, user, rule.GetEvalCondition().WithSource("backtesting"), &schedule.AlertingResultsFromRuleState{ - Manager: stateManager, - Rule: rule, - }) + evaluations := calculateNumberOfEvaluations(firstEval, to, rule.GetInterval()) + if e.maxEvaluations > 0 && evaluations > e.maxEvaluations { + logger.Warn("Evaluations adjusted to maximal number", "originalEvaluations", evaluations, "adjustedEvaluations", e.maxEvaluations) + warns = append(warns, fmt.Sprintf("Number of evaluations are adjusted to the limit of %d evaluations. Requested: %d", e.maxEvaluations, evaluations)) + evaluations = e.maxEvaluations + } + + start := time.Now() + defer func() { + if err == nil { + logger.Info("Rule testing finished successfully", "duration", time.Since(start)) + } else { + logger.Error("Rule testing finished with error", "duration", time.Since(start), "error", err) + } + }() + + stateMgr := e.createStateManager() + + evaluator, err := backtestingEvaluatorFactory(ruleCtx, + e.evalFactory, + user, + rule.GetEvalCondition().WithSource("backtesting"), + &schedule.AlertingResultsFromRuleState{ + Manager: stateMgr, + Rule: rule, + }, + ) if err != nil { return nil, errors.Join(ErrInvalidInputData, err) } - logger.Info("Start testing alert rule", "from", from, "to", to, "interval", rule.IntervalSeconds, "evaluations", length) + logger.Info("Start testing alert rule", "from", from, "to", to, "interval", rule.GetInterval(), "firstTick", firstEval, "evaluations", evaluations, "jitterOffset", jitterOffset, "jitterStrategy", effectiveStrategy) - start := time.Now() + var builder *historian.QueryResultBuilder - tsField := data.NewField("Time", nil, make([]time.Time, length)) - valueFields := make(map[data.Fingerprint]*data.Field) - - err = evaluator.Eval(ruleCtx, from, time.Duration(rule.IntervalSeconds)*time.Second, length, func(idx int, currentTime time.Time, results eval.Results) error { - if idx >= length { - logger.Info("Unexpected evaluation. Skipping", "from", from, "to", to, "interval", rule.IntervalSeconds, "evaluationTime", currentTime, "evaluationIndex", idx, "expectedEvaluations", length) - return nil - } - states := stateManager.ProcessEvalResults(ruleCtx, currentTime, rule, results, nil, nil) - tsField.Set(idx, currentTime) - for _, s := range states { - field, ok := valueFields[s.CacheID] - if !ok { - field = data.NewField("", s.Labels, make([]*string, length)) - valueFields[s.CacheID] = field - } - if s.State.State != eval.NoData { // set nil if NoData - value := s.State.State.String() - if s.StateReason != "" { - value += " (" + s.StateReason + ")" - } - field.Set(idx, &value) - continue - } - } - return nil - }) - fields := make([]*data.Field, 0, len(valueFields)+1) - fields = append(fields, tsField) - for _, f := range valueFields { - fields = append(fields, f) + ruleMeta := history_model.RuleMeta{ + ID: rule.ID, + OrgID: rule.OrgID, + UID: rule.UID, + Title: rule.Title, + Group: rule.RuleGroup, + NamespaceUID: rule.NamespaceUID, + // DashboardUID: "", + // PanelID: 0, + Condition: rule.Condition, } - result := data.NewFrame("Testing results", fields...) - + labels := map[string]string{ + historian.OrgIDLabel: fmt.Sprint(ruleMeta.OrgID), + historian.GroupLabel: fmt.Sprint(ruleMeta.Group), + historian.FolderUIDLabel: fmt.Sprint(rule.NamespaceUID), + } + labelsBytes, err := json.Marshal(labels) if err != nil { return nil, err } - logger.Info("Rule testing finished successfully", "duration", time.Since(start)) - return result, nil + + // Ensure fallback if empty string is passed + if folderTitle == "" { + folderTitle = "Backtesting" + } + extraLabels := state.GetRuleExtraLabels(logger, rule, folderTitle, !e.disableGrafanaFolder, e.featureToggles) + + processFn := func(idx int, currentTime time.Time, results eval.Results) (bool, error) { + // init the builder. Do the best guess for the size of the result + if builder == nil { + builder = historian.NewQueryResultBuilder(evaluations * len(results)) + for _, warn := range warns { + builder.AddWarn(warn) + } + } + states := stateMgr.ProcessEvalResults(ruleCtx, currentTime, rule, results, extraLabels, nil) + for _, s := range states { + if !historian.ShouldRecord(s) { + continue + } + entry := historian.StateTransitionToLokiEntry(ruleMeta, s) + err := builder.AddRow(currentTime, entry, labelsBytes) + if err != nil { + return false, err + } + } + return idx <= evaluations, nil + } + + err = evaluator.Eval(ruleCtx, firstEval, rule.GetInterval(), evaluations, processFn) + if err != nil { + return nil, err + } + if builder == nil { + return nil, errors.New("no results were produced") + } + return builder.ToFrame(), nil } func newBacktestingEvaluator(ctx context.Context, evalFactory eval.EvaluatorFactory, user identity.Requester, condition models.Condition, reader eval.AlertingResultsReader) (backtestingEvaluator, error) { @@ -173,3 +250,53 @@ type NoopImageService struct{} func (s *NoopImageService) NewImage(_ context.Context, _ *models.AlertRule) (*models.Image, error) { return &models.Image{}, nil } + +func getNextEvaluationTime(currentTime time.Time, rule *models.AlertRule, baseInterval time.Duration, jitterOffset time.Duration) (time.Time, error) { + if rule.IntervalSeconds%int64(baseInterval.Seconds()) != 0 { + return time.Time{}, fmt.Errorf("interval %ds is not divisible by base interval %ds", rule.IntervalSeconds, int64(baseInterval.Seconds())) + } + + freq := rule.IntervalSeconds / int64(baseInterval.Seconds()) + + firstTickNum := currentTime.Unix() / int64(baseInterval.Seconds()) + + jitterOffsetTicks := int64(jitterOffset / baseInterval) + + firstEvalTickNum := firstTickNum + (jitterOffsetTicks-(firstTickNum%freq)+freq)%freq + + return time.Unix(firstEvalTickNum*int64(baseInterval.Seconds()), 0), nil +} + +func getFirstEvaluationTime(from time.Time, rule *models.AlertRule, baseInterval time.Duration, jitterOffset time.Duration) (time.Time, error) { + // Now calculate the time of the tick the same way as in the scheduler + firstTick := ticker.GetStartTick(from, baseInterval) + + // calculate time of the first evaluation that is at or after the first tick + firstEval, err := getNextEvaluationTime(firstTick, rule, baseInterval, jitterOffset) + if err != nil { + return time.Time{}, err + } + + // Ensure firstEval is at or after from + // Calculate how many intervals to skip to get past 'from' + if firstEval.Before(from) { + diff := from.Sub(firstEval) + interval := rule.GetInterval() + // Ceiling division: how many intervals needed to cover the difference + intervalsToAdd := (diff + interval - 1) / interval + firstEval = firstEval.Add(interval * intervalsToAdd) + } + + return firstEval, nil +} + +func calculateNumberOfEvaluations(firstEval, to time.Time, interval time.Duration) int { + var evaluations int + if to.After(firstEval) { + evaluations = int(to.Sub(firstEval).Seconds()) / int(interval.Seconds()) + } + if evaluations == 0 { + evaluations = 1 + } + return evaluations +} diff --git a/pkg/services/ngalert/backtesting/engine_test.go b/pkg/services/ngalert/backtesting/engine_test.go index d2685e71535..33441d73f32 100644 --- a/pkg/services/ngalert/backtesting/engine_test.go +++ b/pkg/services/ngalert/backtesting/engine_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "math/rand" "testing" "time" @@ -14,9 +13,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/eval/eval_mocks" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/schedule" "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/util" ) @@ -158,16 +159,6 @@ func TestNewBacktestingEvaluator(t *testing.T) { } func TestEvaluatorTest(t *testing.T) { - states := []eval.State{eval.Normal, eval.Alerting, eval.Pending} - generateState := func(prefix string) *state.State { - labels := models.GenerateAlertLabels(rand.Intn(5)+1, prefix+"-") - return &state.State{ - CacheID: labels.Fingerprint(), - Labels: labels, - State: states[rand.Intn(len(states))], - } - } - randomResultCallback := func(now time.Time) (eval.Results, error) { return eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen()), nil } @@ -189,84 +180,17 @@ func TestEvaluatorTest(t *testing.T) { createStateManager: func() stateManager { return manager }, + disableGrafanaFolder: false, + featureToggles: featuremgmt.WithFeatures(), + minInterval: 1 * time.Second, + baseInterval: 1 * time.Second, + jitterStrategy: schedule.JitterNever, + maxEvaluations: 10000, } gen := models.RuleGen rule := gen.With(gen.WithInterval(time.Second)).GenerateRef() ruleInterval := time.Duration(rule.IntervalSeconds) * time.Second - t.Run("should return data frame in specific format", func(t *testing.T) { - from := time.Unix(0, 0) - to := from.Add(5 * ruleInterval) - allStates := [...]eval.State{eval.Normal, eval.Alerting, eval.Pending, eval.NoData, eval.Error} - - var states []state.StateTransition - - for _, s := range allStates { - labels := models.GenerateAlertLabels(rand.Intn(5)+1, s.String()+"-") - states = append(states, state.StateTransition{ - State: &state.State{ - CacheID: labels.Fingerprint(), - Labels: labels, - State: s, - StateReason: util.GenerateShortUID(), - }, - }) - } - - manager.stateCallback = func(now time.Time) []state.StateTransition { - return states - } - - frame, err := engine.Test(context.Background(), nil, rule, from, to) - - require.NoError(t, err) - require.Len(t, frame.Fields, len(states)+1) // +1 - timestamp - - t.Run("should contain field Time", func(t *testing.T) { - timestampField, _ := frame.FieldByName("Time") - require.NotNil(t, timestampField, "frame does not contain field 'Time'") - require.Equal(t, data.FieldTypeTime, timestampField.Type()) - }) - - fieldByState := make(map[data.Fingerprint]*data.Field, len(states)) - - t.Run("should contain a field per state", func(t *testing.T) { - for _, s := range states { - var f *data.Field - for _, field := range frame.Fields { - if field.Labels.String() == s.Labels.String() { - f = field - break - } - } - require.NotNilf(t, f, "Cannot find a field by state labels") - fieldByState[s.CacheID] = f - } - }) - - t.Run("should be populated with correct values", func(t *testing.T) { - timestampField, _ := frame.FieldByName("Time") - expectedLength := timestampField.Len() - for _, field := range frame.Fields { - require.Equalf(t, expectedLength, field.Len(), "Field %s should have the size %d", field.Name, expectedLength) - } - for i := 0; i < expectedLength; i++ { - expectedTime := from.Add(time.Duration(int64(i)*rule.IntervalSeconds) * time.Second) - require.Equal(t, expectedTime, timestampField.At(i).(time.Time)) - for _, s := range states { - f := fieldByState[s.CacheID] - if s.State.State == eval.NoData { - require.Nil(t, f.At(i)) - } else { - v := f.At(i).(*string) - require.NotNilf(t, v, "Field [%s] value at index %d should not be nil", s.CacheID, i) - require.Equal(t, fmt.Sprintf("%s (%s)", s.State.State, s.StateReason), *v) - } - } - } - }) - }) - t.Run("should not fail if 'to-from' is not times of interval", func(t *testing.T) { from := time.Unix(0, 0) to := from.Add(5 * ruleInterval) @@ -287,84 +211,26 @@ func TestEvaluatorTest(t *testing.T) { return states } - frame, err := engine.Test(context.Background(), nil, rule, from, to) + frame, err := engine.Test(context.Background(), nil, rule, from, to, "") require.NoError(t, err) expectedLen := frame.Rows() for i := 0; i < 100; i++ { jitter := time.Duration(rand.Int63n(ruleInterval.Milliseconds())) * time.Millisecond - frame, err = engine.Test(context.Background(), nil, rule, from, to.Add(jitter)) + frame, err = engine.Test(context.Background(), nil, rule, from, to.Add(jitter), "") require.NoError(t, err) require.Equalf(t, expectedLen, frame.Rows(), "jitter %v caused result to be different that base-line", jitter) } }) - t.Run("should backfill field with nulls if a new dimension created in the middle", func(t *testing.T) { - from := time.Unix(0, 0) - - state1 := state.StateTransition{ - State: generateState("1"), - } - state2 := state.StateTransition{ - State: generateState("2"), - } - state3 := state.StateTransition{ - State: generateState("3"), - } - stateByTime := map[time.Time][]state.StateTransition{ - from: {state1, state2}, - from.Add(1 * ruleInterval): {state1, state2}, - from.Add(2 * ruleInterval): {state1, state2}, - from.Add(3 * ruleInterval): {state1, state2, state3}, - from.Add(4 * ruleInterval): {state1, state2, state3}, - } - to := from.Add(time.Duration(len(stateByTime)) * ruleInterval) - - manager.stateCallback = func(now time.Time) []state.StateTransition { - return stateByTime[now] - } - - frame, err := engine.Test(context.Background(), nil, rule, from, to) - require.NoError(t, err) - - var field3 *data.Field - for _, field := range frame.Fields { - if field.Labels.String() == state3.Labels.String() { - field3 = field - break - } - } - require.NotNilf(t, field3, "Result for state 3 was not found") - require.Equalf(t, len(stateByTime), field3.Len(), "State3 result has unexpected number of values") - - idx := 0 - for curTime, states := range stateByTime { - value := field3.At(idx).(*string) - if len(states) == 2 { - require.Nilf(t, value, "The result should be nil if state3 was not available for time %v", curTime) - } - } - }) - t.Run("should fail", func(t *testing.T) { manager.stateCallback = func(now time.Time) []state.StateTransition { return nil } - t.Run("when interval is not correct", func(t *testing.T) { from := time.Now() - t.Run("when from=to", func(t *testing.T) { - to := from - _, err := engine.Test(context.Background(), nil, rule, from, to) - require.ErrorIs(t, err, ErrInvalidInputData) - }) t.Run("when from > to", func(t *testing.T) { to := from.Add(-ruleInterval) - _, err := engine.Test(context.Background(), nil, rule, from, to) - require.ErrorIs(t, err, ErrInvalidInputData) - }) - t.Run("when to-from < interval", func(t *testing.T) { - to := from.Add(ruleInterval).Add(-time.Millisecond) - _, err := engine.Test(context.Background(), nil, rule, from, to) + _, err := engine.Test(context.Background(), nil, rule, from, to, "") require.ErrorIs(t, err, ErrInvalidInputData) }) }) @@ -376,7 +242,7 @@ func TestEvaluatorTest(t *testing.T) { } from := time.Now() to := from.Add(ruleInterval) - _, err := engine.Test(context.Background(), nil, rule, from, to) + _, err := engine.Test(context.Background(), nil, rule, from, to, "") require.ErrorIs(t, err, expectedError) }) }) @@ -404,10 +270,188 @@ func (f *fakeBacktestingEvaluator) Eval(_ context.Context, from time.Time, inter if err != nil { return err } - err = callback(idx, now, results) + c, err := callback(idx, now, results) if err != nil { return err } + if !c { + break + } } return nil } + +func TestGetNextEvaluationTime(t *testing.T) { + baseInterval := 10 * time.Second + + testCases := []struct { + name string + ruleInterval int64 + currentTimestamp int64 + jitterOffset time.Duration + expectError bool + expectedNext int64 + }{ + { + name: "interval not divisible by base interval", + ruleInterval: 15, + currentTimestamp: 0, + jitterOffset: 0, + expectError: true, + }, + { + name: "no jitter - from tick 0", + ruleInterval: 20, + currentTimestamp: 0, + jitterOffset: 0, + expectedNext: 0, + }, + { + name: "no jitter - from tick 1", + ruleInterval: 20, + currentTimestamp: 10, + jitterOffset: 0, + expectedNext: 20, + }, + { + name: "no jitter - from tick 2", + ruleInterval: 20, + currentTimestamp: 20, + jitterOffset: 0, + expectedNext: 20, + }, + { + name: "with 20s jitter - from tick 0", + ruleInterval: 60, + currentTimestamp: 0, + jitterOffset: 20 * time.Second, + expectedNext: 20, + }, + { + name: "with 20s jitter - from tick 2", + ruleInterval: 60, + currentTimestamp: 20, + jitterOffset: 20 * time.Second, + expectedNext: 20, + }, + { + name: "with 20s jitter - from tick 3", + ruleInterval: 60, + currentTimestamp: 30, + jitterOffset: 20 * time.Second, + expectedNext: 80, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rule := &models.AlertRule{IntervalSeconds: tc.ruleInterval} + currentTime := time.Unix(tc.currentTimestamp, 0) + result, err := getNextEvaluationTime(currentTime, rule, baseInterval, tc.jitterOffset) + + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), "is not divisible by base interval") + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedNext, result.Unix()) + }) + } +} + +func TestGetFirstEvaluationTime(t *testing.T) { + baseInterval := 10 * time.Second + + testCases := []struct { + name string + ruleInterval int64 + fromUnix int64 + jitterOffset time.Duration + expectError bool + expectedUnix int64 + }{ + { + name: "interval not divisible by base interval", + ruleInterval: 15, + fromUnix: 0, + jitterOffset: 0, + expectError: true, + }, + { + name: "no jitter - from at tick 0", + ruleInterval: 20, + fromUnix: 0, + jitterOffset: 0, + expectedUnix: 0, + }, + { + name: "no jitter - from at tick 1", + ruleInterval: 20, + fromUnix: 10, + jitterOffset: 0, + expectedUnix: 20, + }, + { + name: "no jitter - from before first tick", + ruleInterval: 20, + fromUnix: 5, + jitterOffset: 0, + expectedUnix: 20, + }, + { + name: "no jitter - from after first aligned tick", + ruleInterval: 20, + fromUnix: 25, + jitterOffset: 0, + expectedUnix: 40, + }, + { + name: "no jitter - from at tick boundary", + ruleInterval: 10, + fromUnix: 10, + jitterOffset: 0, + expectedUnix: 10, + }, + { + name: "with 20s jitter - from epoch", + ruleInterval: 60, + fromUnix: 0, + jitterOffset: 20 * time.Second, + expectedUnix: 20, + }, + { + name: "with 20s jitter - from 70s", + ruleInterval: 60, + fromUnix: 70, + jitterOffset: 20 * time.Second, + expectedUnix: 80, + }, + { + name: "with 50s jitter - from 25s", + ruleInterval: 60, + fromUnix: 25, + jitterOffset: 50 * time.Second, + expectedUnix: 50, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rule := &models.AlertRule{IntervalSeconds: tc.ruleInterval} + from := time.Unix(tc.fromUnix, 0) + result, err := getFirstEvaluationTime(from, rule, baseInterval, tc.jitterOffset) + + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), "is not divisible by base interval") + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedUnix, result.Unix()) + require.GreaterOrEqual(t, result.Unix(), from.Unix(), "first eval should be at or after from") + }) + } +} diff --git a/pkg/services/ngalert/backtesting/eval_data.go b/pkg/services/ngalert/backtesting/eval_data.go index 999c0bc6302..13827e6e757 100644 --- a/pkg/services/ngalert/backtesting/eval_data.go +++ b/pkg/services/ngalert/backtesting/eval_data.go @@ -85,10 +85,13 @@ func (d *dataEvaluator) Eval(_ context.Context, from time.Time, interval time.Du EvaluatedAt: now, }) } - err := callback(i, now, result) + cont, err := callback(i, now, result) if err != nil { return err } + if !cont { + break + } } return nil } diff --git a/pkg/services/ngalert/backtesting/eval_data_test.go b/pkg/services/ngalert/backtesting/eval_data_test.go index 864229b777c..3d80fa9337a 100644 --- a/pkg/services/ngalert/backtesting/eval_data_test.go +++ b/pkg/services/ngalert/backtesting/eval_data_test.go @@ -100,11 +100,11 @@ func TestDataEvaluator_Eval(t *testing.T) { resultsCount := int(to.Sub(from).Seconds() / interval.Seconds()) - err = evaluator.Eval(context.Background(), from, time.Second, resultsCount, func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), from, time.Second, resultsCount, func(idx int, now time.Time, res eval.Results) (bool, error) { r = append(r, results{ now, res, }) - return nil + return true, nil }) require.NoError(t, err) @@ -164,11 +164,11 @@ func TestDataEvaluator_Eval(t *testing.T) { size := to.Sub(from).Milliseconds() / interval.Milliseconds() r := make([]results, 0, size) - err = evaluator.Eval(context.Background(), from, interval, int(size), func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), from, interval, int(size), func(idx int, now time.Time, res eval.Results) (bool, error) { r = append(r, results{ now, res, }) - return nil + return true, nil }) currentRowIdx := 0 @@ -195,11 +195,11 @@ func TestDataEvaluator_Eval(t *testing.T) { size := int(to.Sub(from).Seconds() / interval.Seconds()) r := make([]results, 0, size) - err = evaluator.Eval(context.Background(), from, interval, size, func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), from, interval, size, func(idx int, now time.Time, res eval.Results) (bool, error) { r = append(r, results{ now, res, }) - return nil + return true, nil }) currentRowIdx := 0 @@ -230,11 +230,11 @@ func TestDataEvaluator_Eval(t *testing.T) { t.Run("should be noData until the frame interval", func(t *testing.T) { newFrom := from.Add(-10 * time.Second) r := make([]results, 0, int(to.Sub(newFrom).Seconds())) - err = evaluator.Eval(context.Background(), newFrom, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), newFrom, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) (bool, error) { r = append(r, results{ now, res, }) - return nil + return true, nil }) rowIdx := 0 @@ -258,11 +258,11 @@ func TestDataEvaluator_Eval(t *testing.T) { t.Run("should be the last value after the frame interval", func(t *testing.T) { newTo := to.Add(10 * time.Second) r := make([]results, 0, int(newTo.Sub(from).Seconds())) - err = evaluator.Eval(context.Background(), from, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), from, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) (bool, error) { r = append(r, results{ now, res, }) - return nil + return true, nil }) rowIdx := 0 @@ -282,12 +282,21 @@ func TestDataEvaluator_Eval(t *testing.T) { }) t.Run("should stop if callback error", func(t *testing.T) { expectedError := errors.New("error") - err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) error { + err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) (bool, error) { if idx == 5 { - return expectedError + return false, expectedError } - return nil + return true, nil }) require.ErrorIs(t, err, expectedError) }) + t.Run("should stop if callback does not want to continue", func(t *testing.T) { + evaluated := 0 + err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) (bool, error) { + evaluated++ + return evaluated < 2, nil + }) + require.NoError(t, err) + require.Equal(t, 2, evaluated) + }) } diff --git a/pkg/services/ngalert/backtesting/eval_query.go b/pkg/services/ngalert/backtesting/eval_query.go index f53e3de86cb..07720f4f265 100644 --- a/pkg/services/ngalert/backtesting/eval_query.go +++ b/pkg/services/ngalert/backtesting/eval_query.go @@ -18,10 +18,13 @@ func (d *queryEvaluator) Eval(ctx context.Context, from time.Time, interval time if err != nil { return err } - err = callback(idx, now, results) + cont, err := callback(idx, now, results) if err != nil { return err } + if !cont { + break + } } return nil } diff --git a/pkg/services/ngalert/backtesting/eval_query_test.go b/pkg/services/ngalert/backtesting/eval_query_test.go index e88948971f0..4c9df9d25b1 100644 --- a/pkg/services/ngalert/backtesting/eval_query_test.go +++ b/pkg/services/ngalert/backtesting/eval_query_test.go @@ -31,9 +31,9 @@ func TestQueryEvaluator_Eval(t *testing.T) { intervals := make([]time.Time, times) - err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error { + err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) (bool, error) { intervals[idx] = now - return nil + return true, nil }) require.NoError(t, err) require.Len(t, intervals, times) @@ -49,7 +49,7 @@ func TestQueryEvaluator_Eval(t *testing.T) { } }) - t.Run("should stop evaluation if error", func(t *testing.T) { + t.Run("should stop evaluation", func(t *testing.T) { t.Run("when evaluation fails", func(t *testing.T) { m := &eval_mocks.ConditionEvaluatorMock{} expectedResults := eval.Results{} @@ -62,9 +62,9 @@ func TestQueryEvaluator_Eval(t *testing.T) { intervals := make([]time.Time, 0, times) - err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error { + err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) (bool, error) { intervals = append(intervals, now) - return nil + return true, nil }) require.ErrorIs(t, err, expectedError) require.Len(t, intervals, 3) @@ -81,14 +81,31 @@ func TestQueryEvaluator_Eval(t *testing.T) { intervals := make([]time.Time, 0, times) - err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error { + err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) (bool, error) { if len(intervals) > 3 { - return expectedError + return false, expectedError } intervals = append(intervals, now) - return nil + return true, nil }) require.ErrorIs(t, err, expectedError) }) + + t.Run("when callback does not want to continue", func(t *testing.T) { + m := &eval_mocks.ConditionEvaluatorMock{} + expectedResults := eval.Results{} + m.EXPECT().Evaluate(mock.Anything, mock.Anything).Return(expectedResults, nil) + evaluator := queryEvaluator{ + eval: m, + } + + evaluated := 0 + err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) (bool, error) { + evaluated++ + return evaluated <= 2, nil + }) + require.NoError(t, err, nil) + require.Equal(t, 3, evaluated) + }) }) } diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 687e9b2dd03..40e2a257e60 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -204,7 +204,7 @@ func IsNonRetryableError(err error) bool { return false } -// HasErrors returns true when Results contains at least one element and all elements are errors +// IsError returns true when Results contains at least one element and all elements are errors func (evalResults Results) IsError() bool { for _, r := range evalResults { if r.State != Error { diff --git a/pkg/services/ngalert/metrics/scheduler.go b/pkg/services/ngalert/metrics/scheduler.go index d6c6707f0a9..9bcff59ffb9 100644 --- a/pkg/services/ngalert/metrics/scheduler.go +++ b/pkg/services/ngalert/metrics/scheduler.go @@ -130,7 +130,7 @@ func NewSchedulerMetrics(r prometheus.Registerer) *Scheduler { Name: "rule_group_rules", Help: "The number of alert rules that are scheduled, by type and state.", }, - []string{"org", "type", "state", "rule_group"}, + []string{"org", "type", "state", "rule_group", "folder_uid"}, ), Groups: promauto.With(r).NewGaugeVec( prometheus.GaugeOpts{ diff --git a/pkg/services/ngalert/models/alert_query.go b/pkg/services/ngalert/models/alert_query.go index 3f03ae2e68f..3c961f8efd4 100644 --- a/pkg/services/ngalert/models/alert_query.go +++ b/pkg/services/ngalert/models/alert_query.go @@ -110,6 +110,12 @@ func (aq *AlertQuery) String() string { } func (aq *AlertQuery) setModelProps() error { + if aq.Model == nil { + // No data to extract, use an empty map. + aq.modelProps = map[string]any{} + return nil + } + aq.modelProps = make(map[string]any) err := json.Unmarshal(aq.Model, &aq.modelProps) if err != nil { diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 7ee223e8165..7d6e8a1fab5 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -18,12 +18,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/prometheus/alertmanager/pkg/labels" prommodels "github.com/prometheus/common/model" "github.com/grafana/grafana-plugin-sdk-go/data" alertingModels "github.com/grafana/alerting/models" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" @@ -397,6 +399,20 @@ type Namespaced interface { type Namespace folder.FolderReference +func NewNamespace(f *folder.Folder) Namespace { + return Namespace(*f.ToFolderReference()) +} + +func (n Namespace) ValidateForRuleStorage() error { + if n.UID == "" { + return fmt.Errorf("cannot store rules in folder without UID") + } + if n.ManagedBy == utils.ManagerKindRepo { + return fmt.Errorf("cannot store rules in folder managed by Git Sync") + } + return nil +} + func (n Namespace) GetNamespaceUID() string { return n.UID } @@ -464,6 +480,10 @@ func (alertRule *AlertRule) GetPanelID() int64 { return -1 } +func (alertRule *AlertRule) GetInterval() time.Duration { + return time.Duration(alertRule.IntervalSeconds) * time.Second +} + type LabelOption func(map[string]string) func WithoutInternalLabels() LabelOption { @@ -997,6 +1017,10 @@ type ListAlertRulesQuery struct { SearchRuleGroup string HasPrometheusRuleDefinition *bool + + // LabelMatchers filters rules by their labels. + // Only equality and inequality matchers are supported, no regex operators. + LabelMatchers labels.Matchers } type ListAlertRulesExtendedQuery struct { @@ -1007,6 +1031,7 @@ type ListAlertRulesExtendedQuery struct { Limit int64 RuleLimit int64 ContinueToken string + Compact bool } // CountAlertRulesQuery is the query for counting alert rules diff --git a/pkg/services/ngalert/models/instance.go b/pkg/services/ngalert/models/instance.go index 8cb42bfc42d..513ce1a3c85 100644 --- a/pkg/services/ngalert/models/instance.go +++ b/pkg/services/ngalert/models/instance.go @@ -9,6 +9,7 @@ import ( type AlertInstance struct { AlertInstanceKey `xorm:"extends"` Labels InstanceLabels + Annotations InstanceAnnotations CurrentState InstanceStateType CurrentReason string CurrentStateSince time.Time diff --git a/pkg/services/ngalert/models/instance_annotations.go b/pkg/services/ngalert/models/instance_annotations.go new file mode 100644 index 00000000000..7eee394ec33 --- /dev/null +++ b/pkg/services/ngalert/models/instance_annotations.go @@ -0,0 +1,34 @@ +package models + +import ( + "encoding/json" +) + +// InstanceAnnotations is an extension to map[string]string with methods +// for database serialization. +type InstanceAnnotations map[string]string + +// FromDB loads annotations stored in the database as JSON into InstanceAnnotations. +// FromDB is part of the xorm Conversion interface. +func (a *InstanceAnnotations) FromDB(b []byte) error { + if len(b) == 0 { + *a = nil + return nil + } + annotations := make(map[string]string) + err := json.Unmarshal(b, &annotations) + if err != nil { + return err + } + *a = annotations + return nil +} + +// ToDB serializes InstanceAnnotations to JSON for database storage. +// ToDB is part of the xorm Conversion interface. +func (a *InstanceAnnotations) ToDB() ([]byte, error) { + if a == nil || len(*a) == 0 { + return nil, nil + } + return json.Marshal(*a) +} diff --git a/pkg/services/ngalert/models/permissions.go b/pkg/services/ngalert/models/permissions.go index a34e184fc99..ce52755eac8 100644 --- a/pkg/services/ngalert/models/permissions.go +++ b/pkg/services/ngalert/models/permissions.go @@ -9,10 +9,11 @@ import ( type ReceiverPermission string const ( - ReceiverPermissionReadSecret ReceiverPermission = "secrets" - ReceiverPermissionAdmin ReceiverPermission = "admin" - ReceiverPermissionWrite ReceiverPermission = "write" - ReceiverPermissionDelete ReceiverPermission = "delete" + ReceiverPermissionReadSecret ReceiverPermission = "secrets" + ReceiverPermissionAdmin ReceiverPermission = "admin" + ReceiverPermissionWrite ReceiverPermission = "write" + ReceiverPermissionDelete ReceiverPermission = "delete" + ReceiverPermissionModifyProtected ReceiverPermission = "modify-protected" ) // ReceiverPermissions returns all possible silence permissions. @@ -22,6 +23,7 @@ func ReceiverPermissions() []ReceiverPermission { ReceiverPermissionAdmin, ReceiverPermissionWrite, ReceiverPermissionDelete, + ReceiverPermissionModifyProtected, } } diff --git a/pkg/services/ngalert/models/receivers_diff.go b/pkg/services/ngalert/models/receivers_diff.go new file mode 100644 index 00000000000..681f07601d9 --- /dev/null +++ b/pkg/services/ngalert/models/receivers_diff.go @@ -0,0 +1,177 @@ +package models + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/alerting/receivers/schema" + + "github.com/grafana/grafana/pkg/util/cmputil" +) + +type IntegrationDiffReport struct { + cmputil.DiffReport +} + +// expandPaths recursively collects all sub-paths for keys in the provided map value +func (r IntegrationDiffReport) expandPaths(basePath schema.IntegrationFieldPath, mapVal reflect.Value) []schema.IntegrationFieldPath { + result := make([]schema.IntegrationFieldPath, 0) + iter := mapVal.MapRange() + for iter.Next() { + keyStr := fmt.Sprintf("%v", iter.Key()) // Assume string keys + p := basePath.With(keyStr) + // Recurse if the sub-value is another map + if m, ok := r.getMap(iter.Value()); ok { + result = append(result, r.expandPaths(p, m)...) + continue + } + result = append(result, p) + } + return result +} + +func (r IntegrationDiffReport) getMap(v reflect.Value) (reflect.Value, bool) { + if v.Kind() == reflect.Map { + return v, true + } + if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + return r.getMap(v.Elem()) + } + return reflect.Value{}, false +} + +func (r IntegrationDiffReport) needExpand(diff cmputil.Diff) (reflect.Value, bool) { + ml, lok := r.getMap(diff.Left) + mr, rok := r.getMap(diff.Right) + if lok == rok { + return reflect.Value{}, false + } + if lok { + return ml, true + } + return mr, true +} + +func (r IntegrationDiffReport) GetSettingsPaths() []schema.IntegrationFieldPath { + diffs := r.GetDiffsForField("Settings") + paths := make([]schema.IntegrationFieldPath, 0, len(diffs)) + for _, diff := range diffs { + // diff.Path has format like Settings[url] or Settings[sub-form][field] + p := diff.Path + var path schema.IntegrationFieldPath + for { + start := strings.Index(p, "[") + if start == -1 { + break + } + p = p[start+1:] + end := strings.Index(p, "]") + if end == -1 { + break + } + fieldName := p[:end] + p = p[end+1:] + path = append(path, fieldName) + } + if m, ok := r.needExpand(diff); ok { + paths = append(paths, r.expandPaths(path, m)...) + continue + } + if len(path) > 0 { + paths = append(paths, path) + } + } + return paths +} + +func (r IntegrationDiffReport) GetSecureSettingsPaths() []schema.IntegrationFieldPath { + diffs := r.GetDiffsForField("SecureSettings") + paths := make([]schema.IntegrationFieldPath, 0, len(diffs)) + for _, diff := range diffs { + if diff.Path == "SecureSettings" { + if m, ok := r.needExpand(diff); ok { + paths = append(paths, r.expandPaths(nil, m)...) + } + continue + } + // diff.Path has format like SecureSettings[field.sub-field.sub] + p := schema.ParseIntegrationPath(diff.Path[len("SecureSettings[") : len(diff.Path)-1]) + paths = append(paths, p) + } + return paths +} + +func (integration *Integration) Diff(incoming Integration) IntegrationDiffReport { + var reporter cmputil.DiffReporter + var settingsCmp = cmpopts.AcyclicTransformer("settingsMap", func(in map[string]any) map[string]any { + if in == nil { + return map[string]any{} + } + return in + }) + var secureCmp = cmpopts.AcyclicTransformer("secureMap", func(in map[string]string) map[string]string { + if in == nil { + return map[string]string{} + } + return in + }) + schemaCmp := cmp.Comparer(func(a, b schema.IntegrationSchemaVersion) bool { + isAZero := reflect.ValueOf(a).IsZero() + isBZero := reflect.ValueOf(b).IsZero() + if isAZero && isBZero { + return true + } + if isAZero || isBZero { + return false + } + return a.Type() == b.Type() && a.Version == b.Version + }) + var cur Integration + if integration != nil { + cur = *integration + } + cmp.Equal(cur, incoming, cmp.Reporter(&reporter), settingsCmp, secureCmp, schemaCmp) + return IntegrationDiffReport{DiffReport: reporter.Diffs} +} + +// HasReceiversDifferentProtectedFields returns true if the receiver has any protected fields that are different from the incoming receiver. +func HasReceiversDifferentProtectedFields(existing, incoming *Receiver) map[string][]schema.IntegrationFieldPath { + existingIntegrations := make(map[string]*Integration, len(existing.Integrations)) + for _, integration := range existing.Integrations { + existingIntegrations[integration.UID] = integration + } + + var result = make(map[string][]schema.IntegrationFieldPath) + for _, in := range incoming.Integrations { + if in.UID == "" { + continue + } + ex, ok := existingIntegrations[in.UID] + if !ok { + continue + } + paths := HasIntegrationsDifferentProtectedFields(ex, in) + if len(paths) > 0 { + result[in.UID] = paths + } + } + return result +} + +// HasIntegrationsDifferentProtectedFields returns list of paths to protected fields that are different between two integrations. +func HasIntegrationsDifferentProtectedFields(existing, incoming *Integration) []schema.IntegrationFieldPath { + diff := existing.Diff(*incoming) + // The incoming receiver always has both secret and non-secret fields in Settings. + // So, if it's specified and happens to be sensitive, we consider it changed + var result []schema.IntegrationFieldPath + settingsDiff := diff.GetSettingsPaths() + for _, path := range settingsDiff { + if incoming.Config.IsProtectedField(path) { + result = append(result, path) + } + } + return result +} diff --git a/pkg/services/ngalert/models/receivers_diff_test.go b/pkg/services/ngalert/models/receivers_diff_test.go new file mode 100644 index 00000000000..dffeaee1dc2 --- /dev/null +++ b/pkg/services/ngalert/models/receivers_diff_test.go @@ -0,0 +1,262 @@ +package models + +import ( + "slices" + "testing" + + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/receivers/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntegrationDiff(t *testing.T) { + s, _ := alertingNotify.GetSchemaVersionForIntegration("webhook", schema.V1) + a := Integration{ + UID: "test-uid", + Name: "test-name", + Config: s, + DisableResolveMessage: false, + Settings: map[string]any{ + "url": "http://localhost", + "name": 123, + "flag": true, + "child": map[string]any{ + "sub-form-field": "test", + }, + }, + SecureSettings: map[string]string{ + "password": "12345", + "token": "token-12345", + }, + } + + t.Run("no diff if equal", func(t *testing.T) { + result := a.Diff(a) + assert.Empty(t, result) + }) + + t.Run("should deep compare settings", func(t *testing.T) { + b := a + b.Settings = map[string]any{ + "url": "http://localhost:123", + "flag": false, + "child": map[string]any{ + "sub-form-field": "test123", + "sub-child": map[string]any{ + "test": "test", + }, + }, + } + + result := a.Diff(b) + assert.ElementsMatch(t, + []string{"Settings[url]", "Settings[name]", "Settings[flag]", "Settings[child][sub-form-field]", "Settings[child][sub-child]"}, + result.Paths()) + }) + + t.Run("should shallow compare schemas", func(t *testing.T) { + b := a + b.Config, _ = alertingNotify.GetSchemaVersionForIntegration("slack", schema.V1) + result := a.Diff(b) + assert.ElementsMatch(t, + []string{"Config"}, + result.Paths()) + }) + + t.Run("should compare with zero objects", func(t *testing.T) { + result := a.Diff(Integration{}) + assert.ElementsMatch(t, + []string{ + "UID", + "Name", + "Config", + "Settings[child]", + "Settings[flag]", + "Settings[name]", + "Settings[url]", + "SecureSettings[password]", + "SecureSettings[token]", + }, + result.Paths()) + }) +} + +func TestIntegrationDiffReport_GetSettingsPaths(t *testing.T) { + a := Integration{ + UID: "test-uid", + Name: "test-name", + Config: schema.IntegrationSchemaVersion{}, + DisableResolveMessage: false, + Settings: map[string]any{ + "url": "http://localhost", + "child": map[string]any{ + "field": "test", + "sub-child": map[string]any{ + "test": "test", + }, + }, + }, + } + + testCases := []struct { + name string + left map[string]any + right map[string]any + paths []string + }{ + { + name: "empty", + left: map[string]any{}, + right: map[string]any{}, + }, + { + name: "left is empty", + left: map[string]any{}, + right: map[string]any{ + "field": "test", + }, + paths: []string{"field"}, + }, + { + name: "right is empty", + left: map[string]any{ + "field": "test", + }, + right: map[string]any{}, + paths: []string{"field"}, + }, + { + name: "expands nested", + left: map[string]any{ + "field": map[string]any{ + "sub-field": map[string]any{ + "test": "test", + }, + }, + }, + right: map[string]any{ + "another": map[string]any{ + "sub-field": map[string]any{ + "test": "test", + }, + }, + }, + paths: []string{ + "field.sub-field.test", + "another.sub-field.test", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := a + b.Settings = tc.right + a.Settings = tc.left + diff := a.Diff(b) + + actual := diff.GetSettingsPaths() + actualStrings := make([]string, 0, len(actual)) + for _, f := range actual { + actualStrings = append(actualStrings, f.String()) + } + assert.ElementsMatch(t, tc.paths, actualStrings) + }) + } +} + +func TestHasDifferentProtectedFields(t *testing.T) { + m := IntegrationMuts + + testCase := []struct { + name string + existing Integration + incoming Integration + expected map[string][]string + }{ + { + name: "different UID do not match", + existing: IntegrationGen(m.WithUID("existing"), m.WithValidConfig("webhook"))(), + incoming: IntegrationGen( + m.WithValidConfig("webhook"), + m.AddSetting("url", "http://some-other-url"), + m.WithUID("incoming"), + )(), + expected: nil, + }, + { + name: "find url protected", + existing: IntegrationGen(m.WithUID("1"), m.WithValidConfig("webhook"))(), + incoming: IntegrationGen( + m.WithValidConfig("webhook"), + m.AddSetting("url", "http://some-other-url"), + m.AddSetting("http_config", map[string]any{ + "oauth2": map[string]any{ + "proxy_config": map[string]any{ + "proxy_url": "http://some-other-url-proxy", + }, + "token_url": "http://some-other-url-token", + }, + }), + m.WithUID("1"), + )(), + expected: map[string][]string{ + "1": { + "http_config.oauth2.proxy_config.proxy_url", + "http_config.oauth2.token_url", + "url", + }, + }, + }, + { + name: "secure and protected", // simulate the situation when protected secured field is in secure settings but the incoming one has it in settings + existing: IntegrationGen( + m.WithUID("1"), + m.WithValidConfig("discord"), + m.RemoveSetting("url"), + m.WithSecureSettings(map[string]string{ + "url": "", + }))(), + incoming: IntegrationGen( + m.WithValidConfig("discord"), + m.AddSetting("url", "http://some-other-url"), + m.WithSecureSettings(nil), + m.WithUID("1"), + )(), + expected: map[string][]string{ + "1": { + "url", + }, + }, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + existing := &Receiver{ + Integrations: []*Integration{ + &tc.existing, + }, + } + incoming := &Receiver{ + Integrations: []*Integration{ + &tc.incoming, + }, + } + actual := HasReceiversDifferentProtectedFields(existing, incoming) + if len(tc.expected) == 0 { + require.Empty(t, actual) + return + } + actualStrings := make(map[string][]string, len(actual)) + for uid, paths := range actual { + for _, path := range paths { + actualStrings[uid] = append(actualStrings[uid], path.String()) + } + slices.Sort(actualStrings[uid]) + } + assert.EqualValues(t, tc.expected, actualStrings) + }) + } +} diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index d2fa78db913..8180afb0012 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -32,9 +32,10 @@ import ( ) var ( - RuleMuts = AlertRuleMutators{} - NSMuts = NotificationSettingsMutators{} - RuleGen = &AlertRuleGenerator{ + RuleMuts = AlertRuleMutators{} + NSMuts = NotificationSettingsMutators{} + InstanceMuts = AlertInstanceMutators{} + RuleGen = &AlertRuleGenerator{ mutators: []AlertRuleMutator{ RuleMuts.WithUniqueUID(), RuleMuts.WithUniqueTitle(), }, @@ -928,6 +929,56 @@ func AlertInstanceGen(mutators ...AlertInstanceMutator) *AlertInstance { return instance } +type AlertInstanceMutators struct{} + +func (a AlertInstanceMutators) WithOrgID(orgID int64) AlertInstanceMutator { + return func(i *AlertInstance) { + i.RuleOrgID = orgID + } +} + +func (a AlertInstanceMutators) WithRuleUID(ruleUID string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.RuleUID = ruleUID + } +} + +func (a AlertInstanceMutators) WithLabelsHash(hash string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.LabelsHash = hash + } +} + +func (a AlertInstanceMutators) WithReason(reason string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.CurrentReason = reason + } +} + +func (a AlertInstanceMutators) WithState(state InstanceStateType) AlertInstanceMutator { + return func(i *AlertInstance) { + i.CurrentState = state + } +} + +func (a AlertInstanceMutators) WithLabels(labels InstanceLabels) AlertInstanceMutator { + return func(i *AlertInstance) { + i.Labels = labels + } +} + +func (a AlertInstanceMutators) WithAnnotations(annotations InstanceAnnotations) AlertInstanceMutator { + return func(i *AlertInstance) { + i.Annotations = annotations + } +} + +func (a AlertInstanceMutators) WithResultFingerprint(fp string) AlertInstanceMutator { + return func(i *AlertInstance) { + i.ResultFingerprint = fp + } +} + type Mutator[T any] func(*T) // CopyNotificationSettings creates a deep copy of NotificationSettings. @@ -1406,6 +1457,12 @@ func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integrati } } +func (n IntegrationMutators) RemoveSetting(key string) Mutator[Integration] { + return func(c *Integration) { + delete(c.Settings, key) + } +} + func randomMapKey[K comparable, V any](m map[K]V) (K, V) { randIdx := rand.Intn(len(m)) i := 0 diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 05e42a66b09..f192ed88058 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/alerting/notify/nfstatus" + alertingTemplates "github.com/grafana/alerting/templates" "github.com/prometheus/alertmanager/config" amv2 "github.com/prometheus/alertmanager/api/v2/models" @@ -58,6 +59,7 @@ type alertmanager struct { decryptFn alertingNotify.GetDecryptedValueFn crypto Crypto features featuremgmt.FeatureToggles + dynamicLimits alertingNotify.DynamicLimits } // maintenanceOptions represent the options for components that need maintenance on a frequency within the Alertmanager. @@ -148,6 +150,16 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A return nil, err } + limits := alertingNotify.DynamicLimits{ + Dispatcher: nilLimits{}, + Templates: alertingTemplates.Limits{ + MaxTemplateOutputSize: cfg.UnifiedAlerting.AlertmanagerMaxTemplateOutputSize, + }, + } + if err := limits.Templates.Validate(); err != nil { + return nil, fmt.Errorf("invalid template limits: %w", err) + } + am := &alertmanager{ Base: gam, ConfigMetrics: m.AlertmanagerConfigMetrics, @@ -158,6 +170,7 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A decryptFn: decryptFn, crypto: crypto, features: featureToggles, + dynamicLimits: limits, } return am, nil @@ -382,7 +395,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable TimeIntervals: amConfig.TimeIntervals, Templates: templates, Receivers: receivers, - DispatcherLimits: &nilLimits{}, + Limits: am.dynamicLimits, Raw: rawConfig, Hash: configHash, }) diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 87d573124e7..ba8d37809b1 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -333,7 +333,7 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte config.ExtraConfigs = extraConfigs } - if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil { + if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers, nil); err != nil { return fmt.Errorf("failed to post process Alertmanager configuration: %w", err) } @@ -485,6 +485,7 @@ func assignReceiverConfigsUIDs(c []*definitions.PostableApiReceiver) error { type provisioningStore interface { GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) + GetProvenancesByUIDs(ctx context.Context, org int64, resourceType string, uids []string) (map[string]models.Provenance, error) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error } diff --git a/pkg/services/ngalert/notifier/crypto.go b/pkg/services/ngalert/notifier/crypto.go index ab5d5f03096..246cd03d64d 100644 --- a/pkg/services/ngalert/notifier/crypto.go +++ b/pkg/services/ngalert/notifier/crypto.go @@ -29,16 +29,18 @@ const ( cryptoPrefix = "crypto_" ) +type AuthorizeProtectedFn func(uid string, paths []schema.IntegrationFieldPath) error + // Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads. type Crypto interface { - LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error + LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver, fn AuthorizeProtectedFn) error Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) Decrypt(ctx context.Context, payload []byte) ([]byte, error) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error getDecryptedSecret(r *definitions.PostableGrafanaReceiver, key string) (string, error) - ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver) error + ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver, fn AuthorizeProtectedFn) error } // alertmanagerCrypto implements decryption of Alertmanager configuration and encryption of arbitrary payloads based on Grafana's encryptions. @@ -57,7 +59,7 @@ func NewCrypto(secrets secrets.Service, configs configurationStore, log log.Logg } // ProcessSecureSettings encrypts new secure settings and loads existing secure settings from the database. -func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver) error { +func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver, authorizeProtected AuthorizeProtectedFn) error { // First, we encrypt the new or updated secure settings. Then, we load the existing secure settings from the database // and add back any that weren't updated. // We perform these steps in this order to ensure the hash of the secure settings remains stable when no secure @@ -68,7 +70,7 @@ func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId in return fmt.Errorf("failed to encrypt receivers: %w", err) } - if err := c.LoadSecureSettings(ctx, orgId, recvs); err != nil { + if err := c.LoadSecureSettings(ctx, orgId, recvs, authorizeProtected); err != nil { return err } @@ -167,7 +169,7 @@ func encryptReceiverConfigs(c []*definitions.PostableApiReceiver, encrypt defini } // LoadSecureSettings adds the corresponding unencrypted secrets stored to the list of input receivers. -func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error { +func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver, authorizeProtected AuthorizeProtectedFn) error { // Get the last known working configuration. amConfig, err := c.configs.GetLatestAlertmanagerConfiguration(ctx, orgId) if err != nil { @@ -176,10 +178,10 @@ func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64 return fmt.Errorf("failed to get latest configuration: %w", err) } } - + var currentConfig *definitions.PostableUserConfig currentReceiverMap := make(map[string]*definitions.PostableGrafanaReceiver) if amConfig != nil { - currentConfig, err := Load([]byte(amConfig.AlertmanagerConfiguration)) + currentConfig, err = Load([]byte(amConfig.AlertmanagerConfiguration)) // If the current config is un-loadable, treat it as if it never existed. Providing a new, valid config should be able to "fix" this state. if err != nil { c.log.Warn("Last known alertmanager configuration was invalid. Overwriting...") @@ -209,6 +211,33 @@ func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64 return UnknownReceiverError{UID: gr.UID} } + if authorizeProtected != nil { + incoming, errIn := legacy_storage.PostableGrafanaReceiverToIntegration(gr) + existing, errEx := legacy_storage.PostableGrafanaReceiverToIntegration(cgmr) + var secure []schema.IntegrationFieldPath + authz := true + if errIn == nil && errEx == nil { + secure = models.HasIntegrationsDifferentProtectedFields(existing, incoming) + authz = len(secure) > 0 + } + // if conversion failed, consider there are changes and authorize + if authz && currentConfig != nil { + var receiverName string + NAME: + for _, rcv := range currentConfig.AlertmanagerConfig.Receivers { + for _, intg := range rcv.GrafanaManagedReceivers { + if intg.UID == cgmr.UID { + receiverName = rcv.Name + break NAME + } + } + } + if err := authorizeProtected(receiverName, secure); err != nil { + return err + } + } + } + // Frontend sends only the secure settings that have to be updated // Therefore we have to copy from the last configuration only those secure settings not included in the request for key, encryptedValue := range cgmr.SecureSettings { diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index 56a68fb3e55..e05b12a2920 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -80,6 +80,9 @@ type receiverAccessControlService interface { AuthorizeUpdate(context.Context, identity.Requester, *models.Receiver) error AuthorizeDeleteByUID(context.Context, identity.Requester, string) error + HasUpdateProtected(context.Context, identity.Requester, *models.Receiver) (bool, error) + AuthorizeUpdateProtected(context.Context, identity.Requester, *models.Receiver) error + Access(ctx context.Context, user identity.Requester, receivers ...*models.Receiver) (map[string]models.ReceiverPermissionSet, error) } @@ -474,6 +477,18 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive return nil, err } + // if user does not have permissions to update protected, check the diff and return error if there is a change in protected fields + canUpdateProtected, _ := rs.authz.HasUpdateProtected(ctx, user, r) + if !canUpdateProtected { + diff := models.HasReceiversDifferentProtectedFields(existing, r) + if len(diff) > 0 { + err = rs.authz.AuthorizeUpdateProtected(ctx, user, r) + if err != nil { + return nil, makeProtectedFieldsAuthzError(err, diff) + } + } + } + // We need to perform two important steps to process settings on an updated integration: // 1. Encrypt new or updated secret fields as they will arrive in plain text. // 2. For updates, callers do not re-send unchanged secure settings and instead mark them in SecureFields. We need diff --git a/pkg/services/ngalert/notifier/receiver_svc_err.go b/pkg/services/ngalert/notifier/receiver_svc_err.go new file mode 100644 index 00000000000..06b27602f75 --- /dev/null +++ b/pkg/services/ngalert/notifier/receiver_svc_err.go @@ -0,0 +1,30 @@ +package notifier + +import ( + "errors" + "slices" + + "github.com/grafana/alerting/receivers/schema" + + "github.com/grafana/grafana/pkg/apimachinery/errutil" +) + +func makeProtectedFieldsAuthzError(err error, diff map[string][]schema.IntegrationFieldPath) error { + var authzErr errutil.Error + if !errors.As(err, &authzErr) { + return err + } + if authzErr.PublicPayload == nil { + authzErr.PublicPayload = map[string]interface{}{} + } + fields := make(map[string][]string, len(diff)) + for field, paths := range diff { + fields[field] = make([]string, len(paths)) + for i, path := range paths { + fields[field][i] = path.String() + } + slices.Sort(fields[field]) + } + authzErr.PublicPayload["changed_protected_fields"] = fields + return authzErr +} diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 8d4b7630436..10e84f33b62 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -659,8 +659,9 @@ func TestReceiverService_Update(t *testing.T) { writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ 1: { - accesscontrol.ActionAlertingNotificationsWrite: nil, - accesscontrol.ActionAlertingNotificationsRead: nil, + accesscontrol.ActionAlertingNotificationsWrite: nil, + accesscontrol.ActionAlertingNotificationsRead: nil, + accesscontrol.ActionAlertingReceiversUpdateProtected: {models.ScopeReceiversAll}, }, }} decryptUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ @@ -1310,7 +1311,7 @@ func TestReceiverServiceAC_Update(t *testing.T) { }, }} - slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack")) + slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("webhook")) emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email")) recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))() recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))() @@ -1322,8 +1323,8 @@ func TestReceiverServiceAC_Update(t *testing.T) { name string permissions map[string][]string existing []models.Receiver - - hasAccess []models.Receiver + incoming []models.Receiver + hasAccess []models.Receiver }{ { name: "not authorized without permissions", @@ -1411,6 +1412,43 @@ func TestReceiverServiceAC_Update(t *testing.T) { existing: allReceivers(), hasAccess: []models.Receiver{recv1, recv3}, }, + { + name: "protected fields modified without permission", + permissions: map[string][]string{ + accesscontrol.ActionAlertingReceiversUpdate: {models.ScopeReceiversAll}, + accesscontrol.ActionAlertingReceiversRead: {models.ScopeReceiversAll}, + }, + existing: []models.Receiver{ + recv1, + }, + incoming: []models.Receiver{ + func() models.Receiver { + f := recv1.Clone() + f.Integrations[0].Settings["url"] = "https://example.com/new" + return f + }(), + }, + hasAccess: nil, + }, + { + name: "protected fields modified with permission", + permissions: map[string][]string{ + accesscontrol.ActionAlertingReceiversUpdate: {models.ScopeReceiversAll}, + accesscontrol.ActionAlertingReceiversRead: {models.ScopeReceiversAll}, + accesscontrol.ActionAlertingReceiversUpdateProtected: {models.ScopeReceiversAll}, + }, + existing: []models.Receiver{ + recv1, + }, + incoming: []models.Receiver{ + func() models.Receiver { + f := recv1.Clone() + f.Integrations[0].Settings["url"] = "https://example.com/new" + return f + }(), + }, + hasAccess: []models.Receiver{recv1}, + }, } for _, tc := range testCases { @@ -1436,7 +1474,11 @@ func TestReceiverServiceAC_Update(t *testing.T) { } return false } - for _, recv := range allReceivers() { + incoming := allReceivers() + if tc.incoming != nil { + incoming = tc.incoming + } + for _, recv := range incoming { clone := recv.Clone() clone.Version = versions[recv.UID] response, err := sut.UpdateReceiver(context.Background(), &clone, nil, orgId, usr) @@ -1734,6 +1776,7 @@ func TestReceiverService_AccessControlMetadata(t *testing.T) { expectedPermissions.Set(models.ReceiverPermissionAdmin, false) expectedPermissions.Set(models.ReceiverPermissionWrite, false) expectedPermissions.Set(models.ReceiverPermissionDelete, false) + expectedPermissions.Set(models.ReceiverPermissionModifyProtected, false) expectedPermissions.Set(models.ReceiverPermissionReadSecret, true) expected := map[string]models.ReceiverPermissionSet{ diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index 13c95e70aa4..0e8ebf647d4 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -272,16 +272,16 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom RuleGroup: promGroup.Name, IsPaused: isPaused, Record: record, + } + + if !isRecordingRule { + result.NotificationSettings = p.cfg.NotificationSettings // MissingSeriesEvalsToResolve is set to 1 to match the Prometheus behaviour. // Prometheus resolves alerts as soon as the series disappears. // By setting this value to 1 we ensure that the alert is resolved on the first evaluation // that doesn't have the series. - MissingSeriesEvalsToResolve: util.Pointer[int64](1), - } - - if !isRecordingRule { - result.NotificationSettings = p.cfg.NotificationSettings + result.MissingSeriesEvalsToResolve = util.Pointer[int64](1) } if p.cfg.KeepOriginalRuleDefinition != nil && *p.cfg.KeepOriginalRuleDefinition { diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 503cd76dd64..d9542e24c5e 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -358,7 +358,12 @@ func TestPrometheusRulesToGrafana(t *testing.T) { require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To) require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From) - require.Equal(t, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve) + + if promRule.Record != "" { + require.Nil(t, grafanaRule.MissingSeriesEvalsToResolve) + } else { + require.Equal(t, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve) + } require.Equal(t, models.OkErrState, grafanaRule.ExecErrState) require.Equal(t, models.OK, grafanaRule.NoDataState) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index aa6c4c55b34..b5ff7506650 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -114,7 +114,7 @@ func (service *AlertRuleService) ListAlertRules(ctx context.Context, user identi } folderUIDs := make([]string, 0, len(folders)) for _, f := range folders { - access, err := service.authz.HasAccessInFolder(ctx, user, models.Namespace(*f.ToFolderReference())) + access, err := service.authz.HasAccessInFolder(ctx, user, models.NewNamespace(f)) if err != nil { return nil, nil, "", err } @@ -407,6 +407,9 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, user ident if err := models.ValidateRuleGroupInterval(intervalSeconds, service.baseIntervalSeconds); err != nil { return err } + if err := service.ensureNamespace(ctx, user, user.GetOrgID(), namespaceUID); err != nil { + return err + } return service.xact.InTransaction(ctx, func(ctx context.Context) error { query := &models.ListAlertRulesQuery{ OrgID: user.GetOrgID(), @@ -471,6 +474,10 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden return err } + if err := service.ensureNamespace(ctx, user, user.GetOrgID(), group.FolderUID); err != nil { + return err + } + // If the rule group is reserved for no-group rules, we cannot have multiple rules in it. if models.IsNoGroupRuleGroup(group.Title) && len(group.Rules) > 1 { return fmt.Errorf("rule group %s is reserved for no-group rules and cannot be used for rule groups with multiple rules", group.Title) @@ -1025,6 +1032,7 @@ func (service *AlertRuleService) checkGroupLimits(group models.AlertRuleGroup) e // ensureNamespace ensures that the rule has a valid namespace UID. // If the rule does not have a namespace UID or the namespace (folder) does not exist it will return an error. +// If the folder is managed by a manager, it will also return an error. func (service *AlertRuleService) ensureNamespace(ctx context.Context, user identity.Requester, orgID int64, namespaceUID string) error { if namespaceUID == "" { return fmt.Errorf("%w: folderUID must be set", models.ErrAlertRuleFailedValidation) @@ -1037,18 +1045,23 @@ func (service *AlertRuleService) ensureNamespace(ctx context.Context, user ident } // ensure the namespace exists - _, err := service.folderService.Get(ctx, &folder.GetFolderQuery{ + f, err := service.folderService.Get(ctx, &folder.GetFolderQuery{ OrgID: orgID, UID: &namespaceUID, SignedInUser: user, }) - if err != nil { + if err != nil || f == nil { if errors.Is(err, dashboards.ErrFolderNotFound) { return fmt.Errorf("%w: folder does not exist", models.ErrAlertRuleFailedValidation) } return err } + // check if the folder is managed by a manager + if err := models.NewNamespace(f).ValidateForRuleStorage(); err != nil { + return fmt.Errorf("%w: %s", models.ErrAlertRuleFailedValidation, err) + } + return nil } diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index c552f797429..ed001a527c4 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" @@ -867,6 +868,27 @@ func TestIntegrationAlertRuleService(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(120), rule.IntervalSeconds) }) + + t.Run("UpdateRuleGroup should reject when folder is managed by a manager", func(t *testing.T) { + service, _, _, ac := initService(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + managedFolderUID := "managed-folder-update-group" + fs := foldertest.NewFakeService() + fs.AddFolder(&folder.Folder{ + OrgID: orgID, + UID: managedFolderUID, + Title: "Managed Folder", + ManagedBy: utils.ManagerKindRepo, + }) + service.folderService = fs + + err := service.UpdateRuleGroup(context.Background(), u, managedFolderUID, "some-group", 120) + require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) + require.ErrorContains(t, err, "cannot store rules in folder managed by Git Sync") + }) } func TestIntegrationCreateAlertRule(t *testing.T) { @@ -1166,6 +1188,30 @@ func TestIntegrationCreateAlertRule(t *testing.T) { require.NoError(t, err) require.True(t, models.IsNoGroupRuleGroup(retrievedRule.RuleGroup), "Rule should be considered NoGroup rule") }) + + t.Run("should reject creation when folder is managed by a manager", func(t *testing.T) { + service, _, _, ac := initService(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + managedFolderUID := "managed-folder" + fs := foldertest.NewFakeService() + fs.AddFolder(&folder.Folder{ + OrgID: orgID, + UID: managedFolderUID, + Title: "Managed Folder", + ManagedBy: utils.ManagerKindRepo, + }) + service.folderService = fs + + rule := dummyRule("test-managed-folder", orgID) + rule.NamespaceUID = managedFolderUID + + _, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone) + require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) + require.ErrorContains(t, err, "cannot store rules in folder managed by Git Sync") + }) } func TestUpdateAlertRule(t *testing.T) { @@ -1316,6 +1362,36 @@ func TestUpdateAlertRule(t *testing.T) { require.Equal(t, "nogroup-update-new", updated.Title) require.Equal(t, originalInterval, updated.IntervalSeconds) }) + + t.Run("should reject update when folder is managed by a manager", func(t *testing.T) { + service, ruleStore, provenanceStore, ac := initService(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + managedFolderUID := "managed-folder-update" + fs := foldertest.NewFakeService() + fs.AddFolder(&folder.Folder{ + OrgID: orgID, + UID: managedFolderUID, + Title: "Managed Folder", + ManagedBy: utils.ManagerKindRepo, + }) + service.folderService = fs + + // Create an existing rule + existingRule := dummyRule("test-managed-folder-update", orgID) + existingRule.NamespaceUID = managedFolderUID + _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: existingRule}}) + require.NoError(t, err) + require.NoError(t, provenanceStore.SetProvenance(context.Background(), &existingRule, orgID, models.ProvenanceNone)) + + // Try to update the rule + existingRule.Title = "Updated Title" + _, err = service.UpdateAlertRule(context.Background(), u, existingRule, models.ProvenanceNone) + require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) + require.ErrorContains(t, err, "cannot store rules in folder managed by Git Sync") + }) } func TestDeleteAlertRule(t *testing.T) { @@ -2054,6 +2130,33 @@ func TestReplaceGroup(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "cannot move rule out of this group") }) + + t.Run("should reject replace when folder is managed by a manager", func(t *testing.T) { + service, _, _, ac := initService(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + managedFolderUID := "managed-folder-replace" + fs := foldertest.NewFakeService() + fs.AddFolder(&folder.Folder{ + OrgID: orgID, + UID: managedFolderUID, + Title: "Managed Folder", + ManagedBy: utils.ManagerKindRepo, + }) + service.folderService = fs + + group := models.AlertRuleGroup{ + Title: "test-group", + FolderUID: managedFolderUID, + Interval: 60, + } + + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "") + require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) + require.ErrorContains(t, err, "cannot store rules in folder managed by Git Sync") + }) } func TestDeleteRuleGroup(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index 407f45efed6..df3683751b3 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -25,6 +26,10 @@ var ( ErrTemplateNotFound = errutil.NotFound("alerting.notifications.templates.notFound") ErrTemplateInvalid = errutil.BadRequest("alerting.notifications.templates.invalidFormat").MustTemplate("Invalid format of the submitted template", errutil.WithPublic("Template is in invalid format. Correct the payload and try again.")) ErrTemplateExists = errutil.BadRequest("alerting.notifications.templates.nameExists", errutil.WithPublicMessage("Template file with this name already exists. Use a different name or update existing one.")) + ErrTemplateOrigin = errutil.BadRequest("alerting.notifications.templates.originInvalid").MustTemplate( + "Template '{{ .Public.Name }}' cannot be {{ .Public.Action }}d because it belongs to an imported configuration.", + errutil.WithPublic("Template '{{ .Public.Name }}' cannot be {{ .Public.Action }}d because it belongs to an imported configuration. Finish the import of the configuration first."), + ) ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy.")) ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules.")) @@ -129,3 +134,7 @@ func MakeErrContactPointUidExists(uid, name string) error { }, }) } + +func makeErrTemplateOrigin(t definitions.NotificationTemplate, action string) error { + return ErrTemplateOrigin.Build(errutil.TemplateData{Public: map[string]interface{}{"Action": action, "Name": t.Name}}) +} diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index 914a3644984..e6d6b37fc24 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -19,6 +19,7 @@ type alertmanagerConfigStore interface { type ProvisioningStore interface { GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) + GetProvenancesByUIDs(ctx context.Context, org int64, resourceType string, uids []string) (map[string]models.Provenance, error) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error } diff --git a/pkg/services/ngalert/provisioning/provisioning_store_mock.go b/pkg/services/ngalert/provisioning/provisioning_store_mock.go index 31cc77e26a6..bbc115d87c1 100644 --- a/pkg/services/ngalert/provisioning/provisioning_store_mock.go +++ b/pkg/services/ngalert/provisioning/provisioning_store_mock.go @@ -188,6 +188,67 @@ func (_c *MockProvisioningStore_GetProvenances_Call) RunAndReturn(run func(conte return _c } +// GetProvenancesByUIDs provides a mock function with given fields: ctx, org, resourceType, uids +func (_m *MockProvisioningStore) GetProvenancesByUIDs(ctx context.Context, org int64, resourceType string, uids []string) (map[string]models.Provenance, error) { + ret := _m.Called(ctx, org, resourceType, uids) + + if len(ret) == 0 { + panic("no return value specified for GetProvenancesByUIDs") + } + + var r0 map[string]models.Provenance + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, string, []string) (map[string]models.Provenance, error)); ok { + return rf(ctx, org, resourceType, uids) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, string, []string) map[string]models.Provenance); ok { + r0 = rf(ctx, org, resourceType, uids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]models.Provenance) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, string, []string) error); ok { + r1 = rf(ctx, org, resourceType, uids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProvisioningStore_GetProvenancesByUIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProvenancesByUIDs' +type MockProvisioningStore_GetProvenancesByUIDs_Call struct { + *mock.Call +} + +// GetProvenancesByUIDs is a helper method to define mock.On call +// - ctx context.Context +// - org int64 +// - resourceType string +// - uids []string +func (_e *MockProvisioningStore_Expecter) GetProvenancesByUIDs(ctx interface{}, org interface{}, resourceType interface{}, uids interface{}) *MockProvisioningStore_GetProvenancesByUIDs_Call { + return &MockProvisioningStore_GetProvenancesByUIDs_Call{Call: _e.mock.On("GetProvenancesByUIDs", ctx, org, resourceType, uids)} +} + +func (_c *MockProvisioningStore_GetProvenancesByUIDs_Call) Run(run func(ctx context.Context, org int64, resourceType string, uids []string)) *MockProvisioningStore_GetProvenancesByUIDs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(string), args[3].([]string)) + }) + return _c +} + +func (_c *MockProvisioningStore_GetProvenancesByUIDs_Call) Return(_a0 map[string]models.Provenance, _a1 error) *MockProvisioningStore_GetProvenancesByUIDs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProvisioningStore_GetProvenancesByUIDs_Call) RunAndReturn(run func(context.Context, int64, string, []string) (map[string]models.Provenance, error)) *MockProvisioningStore_GetProvenancesByUIDs_Call { + _c.Call.Return(run) + return _c +} + // SetProvenance provides a mock function with given fields: ctx, o, org, p func (_m *MockProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error { ret := _m.Called(ctx, o, org, p) diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 9d4db1ed140..65cecfb506b 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -10,6 +10,8 @@ import ( "sort" "unsafe" + "github.com/grafana/alerting/definition" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -23,6 +25,7 @@ type TemplateService struct { xact TransactionManager log log.Logger validator validation.ProvenanceStatusTransitionValidator + includeImported bool } func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService { @@ -32,6 +35,18 @@ func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore, xact: xact, validator: validation.ValidateProvenanceRelaxed, log: log, + includeImported: false, + } +} + +func (t *TemplateService) WithIncludeImported() *TemplateService { + return &TemplateService{ + configStore: t.configStore, + provenanceStore: t.provenanceStore, + xact: t.xact, + validator: t.validator, + log: t.log, + includeImported: true, } } @@ -41,35 +56,38 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi return nil, err } - if len(revision.Config.TemplateFiles) == 0 { - return nil, nil - } + var templates []definitions.NotificationTemplate - provenances, err := t.provenanceStore.GetProvenances(ctx, orgID, (&definitions.NotificationTemplate{}).ResourceType()) - if err != nil { - return nil, err - } - - templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles)) - names := slices.Collect(maps.Keys(revision.Config.TemplateFiles)) - sort.Strings(names) - for _, name := range names { - content := revision.Config.TemplateFiles[name] - tmpl := definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(name), - Name: name, - Template: content, - ResourceVersion: calculateTemplateFingerprint(content), + if len(revision.Config.TemplateFiles) > 0 { + provenances, err := t.provenanceStore.GetProvenances(ctx, orgID, (&definitions.NotificationTemplate{}).ResourceType()) + if err != nil { + return nil, err } - provenance, ok := provenances[tmpl.ResourceID()] - if !ok { - provenance = models.ProvenanceNone + templates = make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles)) + names := slices.Collect(maps.Keys(revision.Config.TemplateFiles)) + sort.Strings(names) + for _, name := range names { + content := revision.Config.TemplateFiles[name] + provenance, ok := provenances[(&definitions.NotificationTemplate{Name: name}).ResourceID()] + if !ok { + provenance = models.ProvenanceNone + } + templates = append(templates, newNotificationTemplate(name, content, provenance, definition.GrafanaTemplateKind)) } - tmpl.Provenance = definitions.Provenance(provenance) - templates = append(templates, tmpl) } - return templates, nil + var importedTemplates []definitions.NotificationTemplate + if t.includeImported && len(revision.Config.ExtraConfigs) > 0 && len(revision.Config.ExtraConfigs[0].TemplateFiles) > 0 { + imported := revision.Config.ExtraConfigs[0].TemplateFiles + importedTemplates = make([]definitions.NotificationTemplate, 0, len(imported)) + names := slices.Collect(maps.Keys(imported)) + sort.Strings(names) + for _, name := range names { + content := imported[name] + templates = append(templates, newNotificationTemplate(name, content, models.ProvenanceConvertedPrometheus, definition.MimirTemplateKind)) + } + } + return append(templates, importedTemplates...), nil } func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, nameOrUid string) (definitions.NotificationTemplate, error) { @@ -77,29 +95,21 @@ func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, nameOrUi if err != nil { return definitions.NotificationTemplate{}, err } - - existingName := nameOrUid - existingContent, ok := revision.Config.TemplateFiles[nameOrUid] - if !ok { - existingName, existingContent, ok = getTemplateByUid(revision.Config.TemplateFiles, nameOrUid) - } - if !ok { - return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("") - } - - tmpl := definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(existingName), - Name: existingName, - Template: existingContent, - ResourceVersion: calculateTemplateFingerprint(existingContent), - } - - provenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID) + result, found, err := t.getTemplateByName(ctx, revision, orgID, nameOrUid) if err != nil { return definitions.NotificationTemplate{}, err } - tmpl.Provenance = definitions.Provenance(provenance) - return tmpl, nil + if found { + return result, nil + } + result, found, err = t.getTemplateByUID(ctx, revision, orgID, nameOrUid) + if err != nil { + return definitions.NotificationTemplate{}, err + } + if found { + return result, nil + } + return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("") } func (t *TemplateService) UpsertTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) { @@ -135,6 +145,10 @@ func (t *TemplateService) CreateTemplate(ctx context.Context, orgID int64, tmpl if err != nil { return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err) } + if tmpl.Kind == definition.MimirTemplateKind { + return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("templates of kind 'Mimir' cannot be created")) + } + revision, err := t.configStore.Get(ctx, orgID) if err != nil { return definitions.NotificationTemplate{}, err @@ -143,6 +157,10 @@ func (t *TemplateService) CreateTemplate(ctx context.Context, orgID int64, tmpl } func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) { + if tmpl.Kind == definition.MimirTemplateKind { + return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("templates of kind 'Mimir' cannot be created")) + } + if revision.Config.TemplateFiles == nil { revision.Config.TemplateFiles = map[string]string{} } @@ -164,13 +182,7 @@ func (t *TemplateService) createTemplate(ctx context.Context, revision *legacy_s return definitions.NotificationTemplate{}, err } - return definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, nil + return newNotificationTemplate(tmpl.Name, tmpl.Template, models.Provenance(tmpl.Provenance), tmpl.Kind), nil } func (t *TemplateService) UpdateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) { @@ -192,37 +204,39 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s } var found bool - var existingName, existingContent string + var err error + var existing definitions.NotificationTemplate // if UID is specified, look by UID. if tmpl.UID != "" { - existingName, existingContent, found = getTemplateByUid(revision.Config.TemplateFiles, tmpl.UID) - // do not fall back to name because we address by UID, and resource can be deleted\renamed + existing, found, err = t.getTemplateByUID(ctx, revision, orgID, tmpl.UID) } else { - existingName = tmpl.Name - existingContent, found = revision.Config.TemplateFiles[existingName] + existing, found, err = t.getTemplateByName(ctx, revision, orgID, tmpl.Name) + } + if err != nil { + return definitions.NotificationTemplate{}, err } if !found { return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("") } - if existingName != tmpl.Name { // if template is renamed, check if this name is already taken + if existing.Name != tmpl.Name { // if template is renamed, check if this name is already taken _, ok := revision.Config.TemplateFiles[tmpl.Name] if ok { // return error if template is being renamed to one that already exists return definitions.NotificationTemplate{}, ErrTemplateExists.Errorf("") } } - - // check that provenance is not changed in an invalid way - storedProvenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID) - if err != nil { - return definitions.NotificationTemplate{}, err + if existing.Kind != tmpl.Kind { + return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(errors.New("cannot change template kind")) } - if err := t.validator(storedProvenance, models.Provenance(tmpl.Provenance)); err != nil { + if existing.Provenance == definitions.Provenance(models.ProvenanceConvertedPrometheus) { + return definitions.NotificationTemplate{}, makeErrTemplateOrigin(existing, "update") + } + if err := t.validator(models.Provenance(existing.Provenance), models.Provenance(tmpl.Provenance)); err != nil { return definitions.NotificationTemplate{}, err } - err = t.checkOptimisticConcurrency(tmpl.Name, existingContent, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update") + err = t.checkOptimisticConcurrency(existing.Name, existing.Template, models.Provenance(tmpl.Provenance), tmpl.ResourceVersion, "update") if err != nil { return definitions.NotificationTemplate{}, err } @@ -230,9 +244,9 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s revision.Config.TemplateFiles[tmpl.Name] = tmpl.Template err = t.xact.InTransaction(ctx, func(ctx context.Context) error { - if existingName != tmpl.Name { // if template by was found by UID and it's name is different, then this is the rename operation. Delete old resources. - delete(revision.Config.TemplateFiles, existingName) - err := t.provenanceStore.DeleteProvenance(ctx, &definitions.NotificationTemplate{Name: existingName}, orgID) + if existing.Name != tmpl.Name { // if template by was found by UID and it's name is different, then this is the rename operation. Delete old resources. + delete(revision.Config.TemplateFiles, existing.Name) + err := t.provenanceStore.DeleteProvenance(ctx, &existing, orgID) if err != nil { return err } @@ -247,13 +261,8 @@ func (t *TemplateService) updateTemplate(ctx context.Context, revision *legacy_s return definitions.NotificationTemplate{}, err } - return definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), // if name was changed, this UID will not match the incoming one - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, nil + // if name was changed, this UID needs to be recalculated + return newNotificationTemplate(tmpl.Name, tmpl.Template, models.Provenance(tmpl.Provenance), tmpl.Kind), nil } func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, nameOrUid string, provenance definitions.Provenance, version string) error { @@ -261,44 +270,39 @@ func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, nameO if err != nil { return err } - - if revision.Config.TemplateFiles == nil { + existing, found, err := t.getTemplateByName(ctx, revision, orgID, nameOrUid) + if err != nil { + return err + } + if !found { + existing, found, err = t.getTemplateByUID(ctx, revision, orgID, nameOrUid) + } + if err != nil { + return err + } + if !found { return nil } - - existingName := nameOrUid - existing, ok := revision.Config.TemplateFiles[nameOrUid] - if !ok { - existingName, existing, ok = getTemplateByUid(revision.Config.TemplateFiles, nameOrUid) - } - if !ok { - return nil + if existing.Provenance == definitions.Provenance(models.ProvenanceConvertedPrometheus) { + return makeErrTemplateOrigin(existing, "delete") } - err = t.checkOptimisticConcurrency(existingName, existing, models.Provenance(provenance), version, "delete") + err = t.checkOptimisticConcurrency(existing.Name, existing.Template, models.Provenance(provenance), version, "delete") if err != nil { return err } - // check that provenance is not changed in an invalid way - storedProvenance, err := t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: existingName}, orgID) - if err != nil { - return err - } - if err = t.validator(storedProvenance, models.Provenance(provenance)); err != nil { + if err = t.validator(models.Provenance(existing.Provenance), models.Provenance(provenance)); err != nil { return err } - delete(revision.Config.TemplateFiles, existingName) + delete(revision.Config.TemplateFiles, existing.Name) return t.xact.InTransaction(ctx, func(ctx context.Context) error { if err := t.configStore.Save(ctx, revision, orgID); err != nil { return err } - tgt := definitions.NotificationTemplate{ - Name: existingName, - } - return t.provenanceStore.DeleteProvenance(ctx, &tgt, orgID) + return t.provenanceStore.DeleteProvenance(ctx, &existing, orgID) }) } @@ -323,11 +327,58 @@ func calculateTemplateFingerprint(t string) string { return fmt.Sprintf("%016x", sum.Sum64()) } -func getTemplateByUid(templates map[string]string, uid string) (string, string, bool) { - for n, tmpl := range templates { - if legacy_storage.NameToUid(n) == uid { - return n, tmpl, true - } +func newNotificationTemplate(name, content string, provenance models.Provenance, kind definition.TemplateKind) definitions.NotificationTemplate { + tmpl := definitions.NotificationTemplate{ + UID: templateUID(kind, name), + Name: name, + Template: content, + Provenance: definitions.Provenance(provenance), + Kind: kind, } - return "", "", false + tmpl.ResourceVersion = calculateTemplateFingerprint(content) + return tmpl +} + +func (t *TemplateService) getTemplateByName(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, name string) (definitions.NotificationTemplate, bool, error) { + existingContent, ok := revision.Config.TemplateFiles[name] + if !ok { + return definitions.NotificationTemplate{}, false, nil + } + provenance, err := t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: name}, orgID) + if err != nil { + return definitions.NotificationTemplate{}, false, err + } + return newNotificationTemplate(name, existingContent, provenance, definition.GrafanaTemplateKind), true, nil +} + +func (t *TemplateService) getTemplateByUID(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64, uid string) (definitions.NotificationTemplate, bool, error) { + find := func(templates map[string]string, uid string, kind definition.TemplateKind) (string, string, bool) { + for n, tmpl := range templates { + if templateUID(kind, n) == uid { + return n, tmpl, true + } + } + return "", "", false + } + var provenance models.Provenance + name, content, ok := find(revision.Config.TemplateFiles, uid, definition.GrafanaTemplateKind) + if !ok { + if t.includeImported && len(revision.Config.ExtraConfigs) > 0 { + name, content, ok = find(revision.Config.ExtraConfigs[0].TemplateFiles, uid, definition.MimirTemplateKind) + if ok { + return newNotificationTemplate(name, content, models.ProvenanceConvertedPrometheus, definition.MimirTemplateKind), true, nil + } + } + return definitions.NotificationTemplate{}, false, nil + } + var err error + provenance, err = t.provenanceStore.GetProvenance(ctx, &definitions.NotificationTemplate{Name: name}, orgID) + if err != nil { + return definitions.NotificationTemplate{}, false, err + } + return newNotificationTemplate(name, content, provenance, definition.GrafanaTemplateKind), true, nil +} + +func templateUID(kind definition.TemplateKind, name string) string { + return legacy_storage.NameToUid(fmt.Sprintf("%s|%s", string(kind), name)) } diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 5b989dd678d..44ced3ba7f9 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -6,6 +6,7 @@ import ( "fmt" "testing" + "github.com/grafana/alerting/definition" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -27,6 +28,15 @@ func TestGetTemplates(t *testing.T) { "template2": "test2", "template3": "test3", }, + ExtraConfigs: []definitions.ExtraConfiguration{ + { + Identifier: "1234", + TemplateFiles: map[string]string{ + "template1": "imported-test1", + "template4": "imported-test4", + }, + }, + }, }, } @@ -45,27 +55,24 @@ func TestGetTemplates(t *testing.T) { require.NoError(t, err) expected := []definitions.NotificationTemplate{ - { - UID: legacy_storage.NameToUid("template1"), - Name: "template1", - Template: "test1", - Provenance: definitions.Provenance(models.ProvenanceAPI), - ResourceVersion: calculateTemplateFingerprint("test1"), - }, - { - UID: legacy_storage.NameToUid("template2"), - Name: "template2", - Template: "test2", - Provenance: definitions.Provenance(models.ProvenanceFile), - ResourceVersion: calculateTemplateFingerprint("test2"), - }, - { - UID: legacy_storage.NameToUid("template3"), - Name: "template3", - Template: "test3", - Provenance: definitions.Provenance(models.ProvenanceNone), - ResourceVersion: calculateTemplateFingerprint("test3"), - }, + newNotificationTemplate( + "template1", + "test1", + models.ProvenanceAPI, + definition.GrafanaTemplateKind, + ), + newNotificationTemplate( + "template2", + "test2", + models.ProvenanceFile, + definition.GrafanaTemplateKind, + ), + newNotificationTemplate( + "template3", + "test3", + models.ProvenanceNone, + definition.GrafanaTemplateKind, + ), } require.EqualValues(t, expected, result) @@ -89,6 +96,60 @@ func TestGetTemplates(t *testing.T) { prov.AssertExpectations(t) }) + t.Run("returns imported templates if enabled", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + sut = sut.WithIncludeImported() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(map[string]models.Provenance{ + "template1": models.ProvenanceAPI, + "template2": models.ProvenanceFile, + }, nil) + + result, err := sut.GetTemplates(context.Background(), orgID) + require.NoError(t, err) + + expected := []definitions.NotificationTemplate{ + newNotificationTemplate( + "template1", + "test1", + models.ProvenanceAPI, + definition.GrafanaTemplateKind, + ), + newNotificationTemplate( + "template2", + "test2", + models.ProvenanceFile, + definition.GrafanaTemplateKind, + ), + newNotificationTemplate( + "template3", + "test3", + models.ProvenanceNone, + definition.GrafanaTemplateKind, + ), + newNotificationTemplate( + "template1", + "imported-test1", + models.ProvenanceConvertedPrometheus, + definition.MimirTemplateKind, + ), + newNotificationTemplate( + "template4", + "imported-test4", + models.ProvenanceConvertedPrometheus, + definition.MimirTemplateKind, + ), + } + + require.EqualValues(t, expected, result) + + prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType()) + prov.AssertExpectations(t) + }) + t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, prov := createTemplateServiceSut() @@ -127,15 +188,25 @@ func TestGetTemplate(t *testing.T) { orgID := int64(1) templateName := "template1" templateContent := "test1" + importedTemplateName := "template2" + importedTemplateContent := "imported" revision := &legacy_storage.ConfigRevision{ Config: &definitions.PostableUserConfig{ TemplateFiles: map[string]string{ templateName: templateContent, }, + ExtraConfigs: []definitions.ExtraConfiguration{ + { + Identifier: "1234", + TemplateFiles: map[string]string{ + importedTemplateName: importedTemplateContent, + }, + }, + }, }, } - t.Run("return a template from config file by name", func(t *testing.T) { + t.Run("return a template from config by name", func(t *testing.T) { sut, store, prov := createTemplateServiceSut() store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { assert.Equal(t, orgID, org) @@ -146,13 +217,12 @@ func TestGetTemplate(t *testing.T) { result, err := sut.GetTemplate(context.Background(), orgID, templateName) require.NoError(t, err) - expected := definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(templateName), - Name: templateName, - Template: templateContent, - Provenance: definitions.Provenance(models.ProvenanceAPI), - ResourceVersion: calculateTemplateFingerprint(templateContent), - } + expected := newNotificationTemplate( + templateName, + templateContent, + models.ProvenanceAPI, + definition.GrafanaTemplateKind, + ) require.Equal(t, expected, result) @@ -162,6 +232,62 @@ func TestGetTemplate(t *testing.T) { prov.AssertExpectations(t) }) + t.Run("imported templates cannot be retrieved by name", func(t *testing.T) { + sut, store, _ := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + _, err := sut.GetTemplate(context.Background(), orgID, importedTemplateName) + require.ErrorIs(t, err, ErrTemplateNotFound) + }) + + t.Run("return a template from config by UID", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + + result, err := sut.GetTemplate(context.Background(), orgID, templateUID(definition.GrafanaTemplateKind, templateName)) + require.NoError(t, err) + + expected := newNotificationTemplate( + templateName, + templateContent, + models.ProvenanceNone, + definition.GrafanaTemplateKind, + ) + require.Equal(t, expected, result) + }) + + t.Run("return an imported template from config by UID", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + + uid := templateUID(definition.MimirTemplateKind, importedTemplateName) + t.Run("should be not found without flag enabled", func(t *testing.T) { + _, err := sut.GetTemplate(context.Background(), orgID, uid) + require.ErrorIs(t, err, ErrTemplateNotFound) + }) + + result, err := sut.WithIncludeImported().GetTemplate(context.Background(), orgID, uid) + require.NoError(t, err) + + expected := newNotificationTemplate( + importedTemplateName, + importedTemplateContent, + models.ProvenanceConvertedPrometheus, + definition.MimirTemplateKind, + ) + require.Equal(t, expected, result) + prov.AssertExpectations(t) + }) + t.Run("returns ErrTemplateNotFound when template does not exist", func(t *testing.T) { sut, store, prov := createTemplateServiceSut() store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { @@ -242,18 +368,18 @@ func TestUpsertTemplate(t *testing.T) { Template: "{{ define \"test\"}} test {{ end }}", Provenance: definitions.Provenance(models.ProvenanceAPI), ResourceVersion: "", + Kind: definition.GrafanaTemplateKind, } result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - require.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + require.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Len(t, store.Calls, 2) @@ -284,18 +410,18 @@ func TestUpsertTemplate(t *testing.T) { Template: "{{ define \"test\"}} test {{ end }}", Provenance: definitions.Provenance(models.ProvenanceAPI), ResourceVersion: calculateTemplateFingerprint("test1"), + Kind: definition.GrafanaTemplateKind, } result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - assert.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + assert.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Len(t, store.Calls, 2) require.Equal(t, "Save", store.Calls[1].Method) @@ -326,13 +452,12 @@ func TestUpsertTemplate(t *testing.T) { result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - assert.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + assert.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + definition.GrafanaTemplateKind, + ), result) require.Equal(t, "Save", store.Calls[1].Method) saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) @@ -356,18 +481,18 @@ func TestUpsertTemplate(t *testing.T) { Template: "content", Provenance: definitions.Provenance(models.ProvenanceNone), ResourceVersion: calculateTemplateFingerprint(currentTemplateContent), + Kind: definition.GrafanaTemplateKind, } result, _ := sut.UpsertTemplate(context.Background(), orgID, tmpl) expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n content\n{{ end }}", templateName) - require.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: expectedContent, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(expectedContent), - }, result) + require.Equal(t, newNotificationTemplate( + tmpl.Name, + expectedContent, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) }) t.Run("does not reject template with unknown field", func(t *testing.T) { @@ -489,6 +614,21 @@ func TestUpsertTemplate(t *testing.T) { require.ErrorIs(t, err, ErrTemplateNotFound) }) + t.Run("rejects new templates of mimir kind", func(t *testing.T) { + sut, store, _ := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + template := definitions.NotificationTemplate{ + Name: "template2", + Template: "asdf-new", + Provenance: definitions.Provenance(models.ProvenanceNone), + Kind: definition.MimirTemplateKind, + } + _, err := sut.UpsertTemplate(context.Background(), orgID, template) + require.ErrorIs(t, err, ErrTemplateInvalid) + }) + t.Run("propagates errors", func(t *testing.T) { tmpl := definitions.NotificationTemplate{ Name: templateName, @@ -562,6 +702,7 @@ func TestCreateTemplate(t *testing.T) { Name: "new-template", Template: "{{ define \"test\"}} test {{ end }}", Provenance: definitions.Provenance(models.ProvenanceAPI), + Kind: definition.GrafanaTemplateKind, } revision := func() *legacy_storage.ConfigRevision { @@ -588,13 +729,12 @@ func TestCreateTemplate(t *testing.T) { result, err := sut.CreateTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - require.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + require.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Len(t, store.Calls, 2) @@ -649,10 +789,33 @@ func TestCreateTemplate(t *testing.T) { require.ErrorIs(t, err, ErrTemplateInvalid) }) + t.Run("invalid kind", func(t *testing.T) { + tmpl := definitions.NotificationTemplate{ + Name: "new-template", + Template: "{{ define \"test\"}} test {{ end }}", + Kind: "unknown", + } + _, err := sut.CreateTemplate(context.Background(), orgID, tmpl) + require.ErrorIs(t, err, ErrTemplateInvalid) + }) + require.Empty(t, store.Calls) prov.AssertExpectations(t) }) + t.Run("rejects templates with mimir kind", func(t *testing.T) { + sut, _, _ := createTemplateServiceSut() + + tmpl := definitions.NotificationTemplate{ + Name: "new-template", + Template: "{{ define \"test\"}} test {{ end }}", + Kind: definition.MimirTemplateKind, + } + + _, err := sut.CreateTemplate(context.Background(), orgID, tmpl) + require.ErrorIs(t, err, ErrTemplateInvalid) + }) + t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, _ := createTemplateServiceSut() @@ -706,6 +869,7 @@ func TestUpdateTemplate(t *testing.T) { Template: "{{ define \"test\"}} test {{ end }}", Provenance: definitions.Provenance(models.ProvenanceAPI), ResourceVersion: "", + Kind: definition.GrafanaTemplateKind, } amConfigToken := util.GenerateShortUID() @@ -771,7 +935,7 @@ func TestUpdateTemplate(t *testing.T) { }, { name: "by uid", - templateUid: legacy_storage.NameToUid(tmpl.UID), + templateUid: templateUID(tmpl.Kind, tmpl.Name), }, } @@ -791,13 +955,12 @@ func TestUpdateTemplate(t *testing.T) { result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - assert.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + assert.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Len(t, store.Calls, 2) require.Equal(t, "Save", store.Calls[1].Method) @@ -821,13 +984,12 @@ func TestUpdateTemplate(t *testing.T) { result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - assert.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + assert.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Equal(t, "Save", store.Calls[1].Method) saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) @@ -853,18 +1015,17 @@ func TestUpdateTemplate(t *testing.T) { oldName := tmpl.Name tmpl := tmpl - tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template - tmpl.Name = "new-template-name" // but name is different + tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template + tmpl.Name = "new-template-name" // but name is different result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl) require.NoError(t, err) - assert.Equal(t, definitions.NotificationTemplate{ - UID: legacy_storage.NameToUid(tmpl.Name), - Name: tmpl.Name, - Template: tmpl.Template, - Provenance: tmpl.Provenance, - ResourceVersion: calculateTemplateFingerprint(tmpl.Template), - }, result) + assert.Equal(t, newNotificationTemplate( + tmpl.Name, + tmpl.Template, + models.Provenance(tmpl.Provenance), + tmpl.Kind, + ), result) require.Len(t, store.Calls, 2) require.Equal(t, "Save", store.Calls[1].Method) @@ -882,6 +1043,7 @@ func TestUpdateTemplate(t *testing.T) { t.Run("rejects rename operation if template with the new name exists", func(t *testing.T) { sut, store, prov := createTemplateServiceSut() + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { return &legacy_storage.ConfigRevision{ Config: &definitions.PostableUserConfig{ @@ -895,8 +1057,8 @@ func TestUpdateTemplate(t *testing.T) { } tmpl := tmpl - tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template - tmpl.Name = "new-template-name" // but name matches another existing template + tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template + tmpl.Name = "new-template-name" // but name matches another existing template _, err := sut.UpdateTemplate(context.Background(), orgID, tmpl) require.ErrorIs(t, err, ErrTemplateExists) @@ -925,6 +1087,16 @@ func TestUpdateTemplate(t *testing.T) { require.ErrorIs(t, err, ErrTemplateInvalid) }) + t.Run("invalid kind", func(t *testing.T) { + tmpl := definitions.NotificationTemplate{ + Name: "", + Template: "", + Kind: "unknown", + } + _, err := sut.UpdateTemplate(context.Background(), orgID, tmpl) + require.ErrorIs(t, err, ErrTemplateInvalid) + }) + require.Empty(t, store.Calls) prov.AssertExpectations(t) }) @@ -975,6 +1147,27 @@ func TestUpdateTemplate(t *testing.T) { prov.AssertExpectations(t) }) + t.Run("rejects existing templates if kind changes", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + + template := definitions.NotificationTemplate{ + Name: "template1", + Template: "asdf-new", + ResourceVersion: "bad-version", + Provenance: definitions.Provenance(models.ProvenanceNone), + Kind: definition.MimirTemplateKind, + } + + _, err := sut.UpdateTemplate(context.Background(), orgID, template) + + require.ErrorIs(t, err, ErrTemplateInvalid) + prov.AssertExpectations(t) + }) + t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, _ := createTemplateServiceSut() @@ -1062,7 +1255,7 @@ func TestDeleteTemplate(t *testing.T) { }, { name: "by uid", - templateNameOrUid: legacy_storage.NameToUid(templateName), + templateNameOrUid: templateUID(definition.GrafanaTemplateKind, templateName), }, } for _, tt := range testCase { @@ -1125,7 +1318,7 @@ func TestDeleteTemplate(t *testing.T) { } t.Run("should look by name before uid", func(t *testing.T) { - expectedToDelete := legacy_storage.NameToUid(templateName) + expectedToDelete := templateUID(definition.GrafanaTemplateKind, templateName) sut, store, prov := createTemplateServiceSut() store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return &legacy_storage.ConfigRevision{ diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 3991df89e77..ebb3a1aa1e9 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -47,10 +47,10 @@ type Rule interface { Identifier() ngmodels.AlertRuleKeyWithGroup } -type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule +type ruleFactoryFunc func(context.Context, ruleWithFolder) Rule -func (f ruleFactoryFunc) new(ctx context.Context, rule *ngmodels.AlertRule) Rule { - return f(ctx, rule) +func (f ruleFactoryFunc) new(ctx context.Context, rf ruleWithFolder) Rule { + return f(ctx, rf) } func newRuleFactory( @@ -70,11 +70,11 @@ func newRuleFactory( evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc, ) ruleFactoryFunc { - return func(ctx context.Context, rule *ngmodels.AlertRule) Rule { - if rule.Type() == ngmodels.RuleTypeRecording { + return func(ctx context.Context, rf ruleWithFolder) Rule { + if rf.rule.Type() == ngmodels.RuleTypeRecording { return newRecordingRule( ctx, - rule.GetKeyWithGroup(), + rf.rule.GetKeyWithGroup(), retryConfig, clock, evalFactory, @@ -89,7 +89,7 @@ func newRuleFactory( } return newAlertRule( ctx, - rule.GetKeyWithGroup(), + rf, appURL, disableGrafanaFolder, retryConfig, @@ -111,7 +111,8 @@ type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time) type stopAppliedFunc = func(ngmodels.AlertRuleKey) type alertRule struct { - key ngmodels.AlertRuleKeyWithGroup + key ngmodels.AlertRuleKeyWithGroup + currentFingerprint fingerprint evalCh chan *Evaluation updateCh chan *Evaluation @@ -139,7 +140,7 @@ type alertRule struct { func newAlertRule( parent context.Context, - key ngmodels.AlertRuleKeyWithGroup, + rf ruleWithFolder, appURL *url.URL, disableGrafanaFolder bool, retryConfig RetryConfig, @@ -154,10 +155,13 @@ func newAlertRule( evalAppliedHook func(ngmodels.AlertRuleKey, time.Time), stopAppliedHook func(ngmodels.AlertRuleKey), ) *alertRule { + key := rf.rule.GetKeyWithGroup() + initialFingerprint := rf.Fingerprint() ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key.AlertRuleKey)) - return &alertRule{ + a := &alertRule{ key: key, + currentFingerprint: initialFingerprint, evalCh: make(chan *Evaluation), updateCh: make(chan *Evaluation), ctx: ctx, @@ -176,6 +180,8 @@ func newAlertRule( tracer: tracer, featureToggles: featureToggles, } + + return a } func (a *alertRule) Identifier() ngmodels.AlertRuleKeyWithGroup { @@ -246,22 +252,23 @@ func (a *alertRule) Run() error { grafanaCtx := a.ctx a.logger.Debug("Alert rule routine started") - var currentFingerprint fingerprint + firstEvalDone := false + defer a.stopApplied() for { select { // used by external services (API) to notify that rule is updated. case ctx := <-a.updateCh: fp := ctx.Fingerprint() - if currentFingerprint == fp { - a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) + if a.currentFingerprint == fp { + a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", a.currentFingerprint) continue } a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.rule.IsPaused, "fingerprint", fp) // clear the state. So the next evaluation will start from the scratch. a.resetState(grafanaCtx, ctx.rule, ctx.rule.IsPaused) - currentFingerprint = fp + a.currentFingerprint = fp // evalCh - used by the scheduler to signal that evaluation is needed. case ctx, ok := <-a.evalCh: if !ok { @@ -295,20 +302,21 @@ func (a *alertRule) Run() error { for { isPaused := ctx.rule.IsPaused - // Do not clean up state if the eval loop has just started. var needReset bool - if currentFingerprint != 0 && currentFingerprint != f { - logger.Debug("Got a new version of alert rule. Clear up the state", "current_fingerprint", currentFingerprint, "fingerprint", f) + if a.currentFingerprint != f { + logger.Debug("Got a new version of alert rule. Clear up the state", "current_fingerprint", a.currentFingerprint, "fingerprint", f) needReset = true } // We need to reset state if the loop has started and the alert is already paused. It can happen, // if we have an alert with state and we do file provision with stateful Grafana, that state // lingers in DB and won't be cleaned up until next alert rule update. - needReset = needReset || (currentFingerprint == 0 && isPaused) + needReset = needReset || (!firstEvalDone && isPaused) if needReset { a.resetState(grafanaCtx, ctx.rule, isPaused) } - currentFingerprint = f + + firstEvalDone = true + a.currentFingerprint = f if isPaused { logger.Debug("Skip rule evaluation because it is paused") return @@ -319,7 +327,7 @@ func (a *alertRule) Run() error { evalTotal.Inc() } - fpStr := currentFingerprint.String() + fpStr := a.currentFingerprint.String() utcTick := ctx.scheduledAt.UTC().Format(time.RFC3339Nano) tracingCtx, span := a.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( attribute.String("rule_uid", ctx.rule.UID), diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 48be6bb4a40..6f4144b37bf 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -334,7 +334,7 @@ func TestAlertRuleAfterEval(t *testing.T) { ruleStore.PutRule(context.Background(), rule) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) return &testContext{ rule: rule, @@ -503,7 +503,14 @@ func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *a Log: log.NewNopLogger(), } st := state.NewManager(managerCfg, state.NewNoopPersister()) - return newAlertRule(ctx, key, nil, false, RetryConfig{}, nil, st, nil, nil, nil, log.NewNopLogger(), nil, featuremgmt.WithFeatures(), nil, nil) + // Create a minimal rule from the key + rule := &models.AlertRule{ + OrgID: key.OrgID, + UID: key.UID, + RuleGroup: key.RuleGroup, + } + rf := ruleWithFolder{rule: rule, folderTitle: ""} + return newAlertRule(ctx, rf, nil, false, RetryConfig{}, nil, st, nil, nil, nil, log.NewNopLogger(), nil, featuremgmt.WithFeatures(), nil, nil) } func TestRuleRoutine(t *testing.T) { @@ -540,7 +547,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { _ = ruleInfo.Run() }() @@ -728,7 +735,8 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) - ruleInfo := factory.new(ctx, rule) + folderTitle := "" + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -750,7 +758,7 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) factory := ruleFactoryFromScheduler(sch) - ruleInfo := factory.new(context.Background(), rule) + ruleInfo := factory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -774,7 +782,7 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) factory := ruleFactoryFromScheduler(sch) - ruleInfo := factory.new(context.Background(), rule) + ruleInfo := factory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -804,7 +812,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { _ = ruleInfo.Run() @@ -871,6 +879,140 @@ func TestRuleRoutine(t *testing.T) { }) }) + t.Run("when update is sent before first evaluation", func(t *testing.T) { + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() + folderTitle := "folderName" + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender, clock.NewMock()) + ruleStore.PutRule(context.Background(), rule) + sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + + // Add state to verify it's not cleared + states := []*state.State{ + { + AlertRuleUID: rule.UID, + CacheID: data.Labels(rule.Labels).Fingerprint(), + OrgID: rule.OrgID, + State: eval.Alerting, + StartsAt: sch.clock.Now(), + EndsAt: sch.clock.Now().Add(5 * time.Second), + Labels: rule.Labels, + }, + } + sch.stateManager.Put(states) + + t.Run("should not reset state if fingerprint is the same", func(t *testing.T) { + factory := ruleFactoryFromScheduler(sch) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + // Send update before first evaluation - same rule, same fingerprint + // This should not reset state since fingerprint is the same + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) + + // Give time for update to be processed + time.Sleep(100 * time.Millisecond) + + actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.NotEmpty(t, actualStates) + }) + + t.Run("should reset state if fingerprint is different", func(t *testing.T) { + // Re-add state for this test + sch.stateManager.Put(states) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch2, ruleStore2, _, _ := createSchedule(make(chan time.Time), sender, clock.NewMock()) + ruleStore2.PutRule(context.Background(), rule) + sch2.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + sch2.stateManager.Put(states) + + factory := ruleFactoryFromScheduler(sch2) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + // Send update before first eval, with a changed alert rule title. + // This should reset state and send resolved alerts + updatedRule := models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())) + ruleInfo.Update(&Evaluation{rule: updatedRule, folderTitle: folderTitle}) + + // Wait for sender to be called (which happens when state is cleared and resolved alerts are sent) + require.Eventually(t, func() bool { + return len(sender.Calls()) > 0 + }, 5*time.Second, 100*time.Millisecond) + + // State should be cleared because fingerprint changed + actualStates := sch2.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Empty(t, actualStates) + }) + }) + + t.Run("paused rule should reset state on first evaluation", func(t *testing.T) { + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() + rule.IsPaused = true + folderTitle := "folderName" + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(make(chan time.Time), sender, clock.NewMock()) + ruleStore.PutRule(context.Background(), rule) + sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + + states := []*state.State{ + { + AlertRuleUID: rule.UID, + CacheID: data.Labels(rule.Labels).Fingerprint(), + OrgID: rule.OrgID, + State: eval.Alerting, + StartsAt: sch.clock.Now(), + EndsAt: sch.clock.Now().Add(5 * time.Second), + Labels: rule.Labels, + }, + } + sch.stateManager.Put(states) + require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + + factory := ruleFactoryFromScheduler(sch) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + ruleInfo.Eval(&Evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + folderTitle: folderTitle, + }) + + require.Eventually(t, func() bool { + return len(sender.Calls()) > 0 + }, 5*time.Second, 100*time.Millisecond) + + actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Empty(t, actualStates) + }) + t.Run("when evaluation fails", func(t *testing.T) { rule := gen.With(withQueryForState(t, eval.Error)).GenerateRef() rule.ExecErrState = models.ErrorErrState @@ -910,7 +1052,7 @@ func TestRuleRoutine(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1044,7 +1186,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1078,7 +1220,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1119,7 +1261,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1214,7 +1356,7 @@ func TestAlertRuleRetry(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() diff --git a/pkg/services/ngalert/schedule/jitter.go b/pkg/services/ngalert/schedule/jitter.go index 3d6c839f372..a805ab9b0b3 100644 --- a/pkg/services/ngalert/schedule/jitter.go +++ b/pkg/services/ngalert/schedule/jitter.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/featuremgmt" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" @@ -13,6 +14,10 @@ import ( // JitterStrategy represents a modifier to alert rule timing that affects how evaluations are distributed. type JitterStrategy int +func (s JitterStrategy) String() string { + return [...]string{"never", "by group", "by rule"}[s] +} + const ( JitterNever JitterStrategy = iota JitterByGroup @@ -57,6 +62,11 @@ func jitterOffsetInTicks(r *ngmodels.AlertRule, baseInterval time.Duration, stra return res } +// JitterOffsetInDuration gives the jitter offset for a rule, in terms of a duration relative to its interval and a base interval. +func JitterOffsetInDuration(r *ngmodels.AlertRule, baseInterval time.Duration, strategy JitterStrategy) time.Duration { + return time.Duration(jitterOffsetInTicks(r, baseInterval, strategy)) * baseInterval +} + func jitterHash(r *ngmodels.AlertRule, strategy JitterStrategy) uint64 { ls := data.Labels{ "name": r.RuleGroup, diff --git a/pkg/services/ngalert/schedule/metrics.go b/pkg/services/ngalert/schedule/metrics.go index e3281c33710..f4630b39cad 100644 --- a/pkg/services/ngalert/schedule/metrics.go +++ b/pkg/services/ngalert/schedule/metrics.go @@ -117,7 +117,7 @@ func (sch *schedule) updateRulesMetrics(alertRules []*models.AlertRule) { // Set metrics for key, count := range buckets { - sch.metrics.GroupRules.WithLabelValues(fmt.Sprint(key.orgID), key.ruleType.String(), key.state, makeRuleGroupLabelValue(key.ruleGroup)).Set(float64(count)) + sch.metrics.GroupRules.WithLabelValues(fmt.Sprint(key.orgID), key.ruleType.String(), key.state, makeRuleGroupLabelValue(key.ruleGroup), key.ruleGroup.NamespaceUID).Set(float64(count)) } for orgID, numRulesNfSettings := range orgsNfSettings { sch.metrics.SimpleNotificationRules.WithLabelValues(fmt.Sprint(orgID)).Set(float64(numRulesNfSettings)) diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index e8a61979d19..da25c38a2df 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -239,7 +239,7 @@ func TestRecordingRuleAfterEval(t *testing.T) { ruleStore.PutRule(context.Background(), rule) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time, 1) // Buffer to avoid blocking afterEvalCh := make(chan struct{}, 1) // Buffer to avoid blocking @@ -444,7 +444,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -583,7 +583,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -728,7 +728,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -787,7 +787,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index ede71c94d75..6ce09d49169 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -21,7 +21,7 @@ var ( ) type ruleFactory interface { - new(context.Context, *models.AlertRule) Rule + new(context.Context, ruleWithFolder) Rule } type ruleRegistry struct { @@ -35,14 +35,14 @@ func newRuleRegistry() ruleRegistry { // getOrCreate gets a rule routine from registry for the provided rule. If it does not exist, it creates a new one. // Returns a pointer to the rule routine and a flag that indicates whether it is a new struct or not. -func (r *ruleRegistry) getOrCreate(context context.Context, item *models.AlertRule, factory ruleFactory) (Rule, bool) { +func (r *ruleRegistry) getOrCreate(context context.Context, rf ruleWithFolder, factory ruleFactory) (Rule, bool) { r.mu.Lock() defer r.mu.Unlock() - key := item.GetKey() + key := rf.rule.GetKey() rule, ok := r.rules[key] if !ok { - rule = factory.new(context, item) + rule = factory.new(context, rf) r.rules[key] = rule } return rule, !ok @@ -101,13 +101,13 @@ func (e *Evaluation) Fingerprint() fingerprint { type alertRulesRegistry struct { rules map[models.AlertRuleKey]*models.AlertRule folderTitles map[models.FolderKey]string - mu sync.Mutex + mu sync.RWMutex } // all returns all rules in the registry. func (r *alertRulesRegistry) all() ([]*models.AlertRule, map[models.FolderKey]string) { - r.mu.Lock() - defer r.mu.Unlock() + r.mu.RLock() + defer r.mu.RUnlock() result := make([]*models.AlertRule, 0, len(r.rules)) for _, rule := range r.rules { result = append(result, rule) @@ -116,8 +116,8 @@ func (r *alertRulesRegistry) all() ([]*models.AlertRule, map[models.FolderKey]st } func (r *alertRulesRegistry) get(k models.AlertRuleKey) *models.AlertRule { - r.mu.Lock() - defer r.mu.Unlock() + r.mu.RLock() + defer r.mu.RUnlock() return r.rules[k] } @@ -157,12 +157,14 @@ func (r *alertRulesRegistry) del(k models.AlertRuleKey) (*models.AlertRule, bool } func (r *alertRulesRegistry) isEmpty() bool { - r.mu.Lock() - defer r.mu.Unlock() + r.mu.RLock() + defer r.mu.RUnlock() return len(r.rules) == 0 } func (r *alertRulesRegistry) needsUpdate(keys []models.AlertRuleKeyWithVersion) bool { + r.mu.RLock() + defer r.mu.RUnlock() if len(r.rules) != len(keys) { return true } diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 44dced0cdea..ba12fe33461 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -320,10 +320,22 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. sch.stopAppliedFunc, ) for _, item := range alertRules { - ruleRoutine, newRoutine := sch.registry.getOrCreate(ctx, item, ruleFactory) key := item.GetKey() logger := sch.log.FromContext(ctx).New(key.LogContext()...) + var folderTitle string + if !sch.disableGrafanaFolder { + title, ok := folderTitles[item.GetFolderKey()] + if ok { + folderTitle = title + } else { + missingFolder[item.NamespaceUID] = append(missingFolder[item.NamespaceUID], item.UID) + } + } + + rf := ruleWithFolder{rule: item, folderTitle: folderTitle} + ruleRoutine, newRoutine := sch.registry.getOrCreate(ctx, rf, ruleFactory) + // enforce minimum evaluation interval if item.IntervalSeconds < int64(sch.minRuleInterval.Seconds()) { logger.Debug("Interval adjusted", "originalInterval", item.IntervalSeconds, "adjustedInterval", sch.minRuleInterval.Seconds()) @@ -337,7 +349,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. logger.Debug("Rule restarted because type changed", "old", ruleRoutine.Type(), "new", item.Type()) restartedRules = append(restartedRules, ruleRoutine) sch.registry.del(key) - ruleRoutine, newRoutine = sch.registry.getOrCreate(ctx, item, ruleFactory) + ruleRoutine, newRoutine = sch.registry.getOrCreate(ctx, rf, ruleFactory) } if newRoutine && !invalidInterval { @@ -357,16 +369,6 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. offset := jitterOffsetInTicks(item, sch.baseInterval, sch.jitterEvaluations) isReadyToRun := item.IntervalSeconds != 0 && (tickNum%itemFrequency)-offset == 0 - var folderTitle string - if !sch.disableGrafanaFolder { - title, ok := folderTitles[item.GetFolderKey()] - if ok { - folderTitle = title - } else { - missingFolder[item.NamespaceUID] = append(missingFolder[item.NamespaceUID], item.UID) - } - } - if isReadyToRun { logger.Debug("Rule is ready to run on the current tick", "tick", tick, "frequency", itemFrequency, "offset", offset) readyToRun = append(readyToRun, readyToRunItem{ruleRoutine: ruleRoutine, Evaluation: Evaluation{ @@ -378,12 +380,12 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if _, isUpdated := updated[key]; isUpdated && !isReadyToRun { // if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that logger.Debug("Rule has been updated. Notifying evaluation routine") - go func(routine Rule, rule *ngmodels.AlertRule) { + go func(routine Rule, rule *ngmodels.AlertRule, folder string) { routine.Update(&Evaluation{ rule: rule, - folderTitle: folderTitle, + folderTitle: folder, }) - }(ruleRoutine, item) + }(ruleRoutine, item, folderTitle) updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{ Version: item.Version, AlertRuleKey: item.GetKey(), diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index c90c9b19707..bbfa20fceb1 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -157,8 +157,8 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1) + grafana_alerting_rule_group_rules{folder_uid="%[3]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, alertRule1.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -199,9 +199,9 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -265,9 +265,9 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="paused",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="paused",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -308,10 +308,10 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="paused",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="paused",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="paused",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="paused",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) }) @@ -336,9 +336,9 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -363,8 +363,8 @@ func TestProcessTicks(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[3]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup2, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) }) @@ -684,8 +684,8 @@ func TestSchedule_updateRulesMetrics(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active", type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1) + grafana_alerting_rule_group_rules{folder_uid="%[3]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, alertRule1.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -705,9 +705,9 @@ func TestSchedule_updateRulesMetrics(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -720,9 +720,9 @@ func TestSchedule_updateRulesMetrics(t *testing.T) { expectedMetric := fmt.Sprintf( `# HELP grafana_alerting_rule_group_rules The number of alert rules that are scheduled, by type and state. # TYPE grafana_alerting_rule_group_rules gauge - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 - grafana_alerting_rule_group_rules{org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 - `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2) + grafana_alerting_rule_group_rules{folder_uid="%[4]s",org="%[1]d",rule_group="%[2]s",state="active",type="alerting"} 1 + grafana_alerting_rule_group_rules{folder_uid="%[5]s",org="%[1]d",rule_group="%[3]s",state="active",type="alerting"} 1 + `, alertRule1.OrgID, folderWithRuleGroup1, folderWithRuleGroup2, alertRule1.NamespaceUID, alertRule2.NamespaceUID) err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_group_rules") require.NoError(t, err) @@ -1107,7 +1107,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) sch.deleteAlertRule(ctx, key) @@ -1126,7 +1126,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) @@ -1149,7 +1149,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) @@ -1172,7 +1172,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { ruleFactory := ruleFactoryFromScheduler(sch) rule := models.RuleGen.GenerateRef() key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) diff --git a/pkg/services/ngalert/schedule/ticker/ticker.go b/pkg/services/ngalert/schedule/ticker/ticker.go index a52b9c4e559..c24dc13798a 100644 --- a/pkg/services/ngalert/schedule/ticker/ticker.go +++ b/pkg/services/ngalert/schedule/ticker/ticker.go @@ -44,7 +44,11 @@ func New(c clock.Clock, interval time.Duration, metric *Metrics, logger log.Logg } func getStartTick(clk clock.Clock, interval time.Duration) time.Time { - nano := clk.Now().UnixNano() + return GetStartTick(clk.Now(), interval) +} + +func GetStartTick(t time.Time, interval time.Duration) time.Time { + nano := t.UnixNano() return time.Unix(0, nano-(nano%interval.Nanoseconds())) } diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index c1598d4aa3b..b52a8a5fd6c 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -341,6 +341,7 @@ func (c *cache) GetAlertInstances() []ngModels.AlertInstance { states = append(states, ngModels.AlertInstance{ AlertInstanceKey: key, Labels: ngModels.InstanceLabels(v2.Labels), + Annotations: v2.Annotations, CurrentState: ngModels.InstanceStateType(v2.State.String()), CurrentReason: v2.StateReason, LastEvalTime: v2.LastEvaluationTime, diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index d2353ec42d8..d9cdfaa9d2b 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -44,6 +44,7 @@ type AnnotationBackend struct { type RuleStore interface { GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) + GetAlertRuleVersionFolders(ctx context.Context, orgID int64, guid string) ([]string, error) } type AnnotationStore interface { diff --git a/pkg/services/ngalert/state/historian/core.go b/pkg/services/ngalert/state/historian/core.go index eabb5214a9e..daf415be25a 100644 --- a/pkg/services/ngalert/state/historian/core.go +++ b/pkg/services/ngalert/state/historian/core.go @@ -17,7 +17,7 @@ import ( const StateHistoryWriteTimeout = time.Minute -func shouldRecord(transition state.StateTransition) bool { +func ShouldRecord(transition state.StateTransition) bool { if !transition.Changed() { return false } @@ -35,9 +35,9 @@ func shouldRecord(transition state.StateTransition) bool { } // ShouldRecordAnnotation returns true if an annotation should be created for a given state transition. -// This is stricter than shouldRecord to avoid cluttering panels with state transitions. +// This is stricter than ShouldRecord to avoid cluttering panels with state transitions. func ShouldRecordAnnotation(t state.StateTransition) bool { - if !shouldRecord(t) { + if !ShouldRecord(t) { return false } diff --git a/pkg/services/ngalert/state/historian/core_test.go b/pkg/services/ngalert/state/historian/core_test.go index f798bd26cef..a5f0c55d816 100644 --- a/pkg/services/ngalert/state/historian/core_test.go +++ b/pkg/services/ngalert/state/historian/core_test.go @@ -92,7 +92,7 @@ func TestShouldRecord(t *testing.T) { } t.Run(fmt.Sprintf("%s -> %s should be %v", trans.PreviousFormatted(), trans.Formatted(), !ok), func(t *testing.T) { - require.Equal(t, !ok, shouldRecord(trans)) + require.Equal(t, !ok, ShouldRecord(trans)) }) } } diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 137c18f031c..8e98d411c1e 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -41,6 +41,69 @@ const ( dfLabels = "labels" ) +// QueryResultBuilder is a builder for a data frame that represents query results from Loki. +// It contains three fields: time (timestamp), line (JSON data), and labels (JSON labels). +type QueryResultBuilder struct { + frame *data.Frame +} + +// NewQueryResultBuilder creates a new QueryResultBuilder with the specified capacity. +// The capacity is used to pre-allocate the underlying slices for better performance. +func NewQueryResultBuilder(capacity int) *QueryResultBuilder { + frame := data.NewFrame("states") + lbls := data.Labels(map[string]string{}) + + // We represent state history as a single merged history, that roughly corresponds to what you get in the Grafana Explore tab when querying Loki directly. + // The format is composed of the following vectors: + // 1. `time` - timestamp - when the transition happened + // 2. `line` - JSON - the full data of the transition + // 3. `labels` - JSON - the labels associated with that state transition + times := make([]time.Time, 0, capacity) + lines := make([]json.RawMessage, 0, capacity) + labels := make([]json.RawMessage, 0, capacity) + + frame.Fields = append(frame.Fields, data.NewField(dfTime, lbls, times)) + frame.Fields = append(frame.Fields, data.NewField(dfLine, lbls, lines)) + frame.Fields = append(frame.Fields, data.NewField(dfLabels, lbls, labels)) + + return &QueryResultBuilder{frame: frame} +} + +func (qr QueryResultBuilder) AddRowRaw(timestamp time.Time, line json.RawMessage, labels json.RawMessage) { + frame := qr.frame + frame.Fields[0].Append(timestamp) + frame.Fields[1].Append(line) + frame.Fields[2].Append(labels) +} + +func (qr QueryResultBuilder) AddRow(timestamp time.Time, line LokiEntry, labels json.RawMessage) error { + lineBytes, err := json.Marshal(line) + if err != nil { + return err + } + qr.AddRowRaw(timestamp, lineBytes, labels) + return nil +} + +// ToFrame converts the QueryResultBuilder back to a data.Frame. +func (qr QueryResultBuilder) ToFrame() *data.Frame { + return qr.frame +} + +func (qr QueryResultBuilder) AddWarn(s string) { + m := qr.frame.Meta + if m == nil { + m = &data.FrameMeta{} + qr.frame.SetMeta(m) + } + m.Notices = append(m.Notices, data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: s, + Link: "", + Inspect: 0, + }) +} + const ( StateHistoryLabelKey = "from" StateHistoryLabelValue = "state-history" @@ -191,20 +254,7 @@ func (h RemoteLokiBackend) merge(res []lokiclient.Stream, folderUIDToFilter []st totalLen += len(arr.Values) } - // Create a new slice to store the merged elements. - frame := data.NewFrame("states") - - // We merge all series into a single linear history. - lbls := data.Labels(map[string]string{}) - - // We represent state history as a single merged history, that roughly corresponds to what you get in the Grafana Explore tab when querying Loki directly. - // The format is composed of the following vectors: - // 1. `time` - timestamp - when the transition happened - // 2. `line` - JSON - the full data of the transition - // 3. `labels` - JSON - the labels associated with that state transition - times := make([]time.Time, 0, totalLen) - lines := make([]json.RawMessage, 0, totalLen) - labels := make([]json.RawMessage, 0, totalLen) + queryResult := NewQueryResultBuilder(totalLen) // Initialize a slice of pointers to the current position in each array. pointers := make([]int, len(res)) @@ -259,17 +309,10 @@ func (h RemoteLokiBackend) merge(res []lokiclient.Stream, folderUIDToFilter []st pointers[minElStreamIdx]++ continue } - times = append(times, time.Unix(0, tsNano)) - labels = append(labels, lblsJson) - lines = append(lines, json.RawMessage(entryBytes)) + queryResult.AddRowRaw(time.Unix(0, tsNano), entryBytes, lblsJson) pointers[minElStreamIdx]++ } - - frame.Fields = append(frame.Fields, data.NewField(dfTime, lbls, times)) - frame.Fields = append(frame.Fields, data.NewField(dfLine, lbls, lines)) - frame.Fields = append(frame.Fields, data.NewField(dfLabels, lbls, labels)) - - return frame, nil + return queryResult.ToFrame(), nil } func StatesToStream(rule history_model.RuleMeta, states []state.StateTransition, externalLabels map[string]string, logger log.Logger) lokiclient.Stream { @@ -282,28 +325,11 @@ func StatesToStream(rule history_model.RuleMeta, states []state.StateTransition, samples := make([]lokiclient.Sample, 0, len(states)) for _, state := range states { - if !shouldRecord(state) { + if !ShouldRecord(state) { continue } - sanitizedLabels := removePrivateLabels(state.Labels) - entry := LokiEntry{ - SchemaVersion: 1, - Previous: state.PreviousFormatted(), - Current: state.Formatted(), - Values: valuesAsDataBlob(state.State), - Condition: rule.Condition, - DashboardUID: rule.DashboardUID, - PanelID: rule.PanelID, - Fingerprint: labelFingerprint(sanitizedLabels), - RuleTitle: rule.Title, - RuleID: rule.ID, - RuleUID: rule.UID, - InstanceLabels: sanitizedLabels, - } - if state.State.State == eval.Error { - entry.Error = state.Error.Error() - } + entry := StateTransitionToLokiEntry(rule, state) jsn, err := json.Marshal(entry) if err != nil { @@ -324,6 +350,28 @@ func StatesToStream(rule history_model.RuleMeta, states []state.StateTransition, } } +func StateTransitionToLokiEntry(rule history_model.RuleMeta, state state.StateTransition) LokiEntry { + sanitizedLabels := removePrivateLabels(state.Labels) + entry := LokiEntry{ + SchemaVersion: 1, + Previous: state.PreviousFormatted(), + Current: state.Formatted(), + Values: valuesAsDataBlob(state.State), + Condition: rule.Condition, + DashboardUID: rule.DashboardUID, + PanelID: rule.PanelID, + Fingerprint: labelFingerprint(sanitizedLabels), + RuleTitle: rule.Title, + RuleID: rule.ID, + RuleUID: rule.UID, + InstanceLabels: sanitizedLabels, + } + if state.State.State == eval.Error && state.Error != nil { + entry.Error = state.Error.Error() + } + return entry +} + func (h *RemoteLokiBackend) recordStreams(ctx context.Context, stream lokiclient.Stream, logger log.Logger) error { if err := h.client.Push(ctx, []lokiclient.Stream{stream}); err != nil { return err @@ -505,32 +553,27 @@ func (h *RemoteLokiBackend) getFolderUIDsForFilter(ctx context.Context, query mo if err != nil { return nil, err } - if bypass { // if user has access to all rules and folder, remove filter + + if query.RuleUID != "" { + return h.getFolderUIDsForRuleFilter(ctx, query, bypass) + } + + // If the query has no rule filter, we need to return all folder UIDs the user has access to. + // For a user with access to all rules and folders, the full list of folders will likely be too large to be an + // effective optimization in Loki, so we skip folderUID filtering entirely in that case. + if bypass { return nil, nil } - // if there is a filter by rule UID, find that rule UID and make sure that user has access to it. - if query.RuleUID != "" { - rule, err := h.ruleStore.GetAlertRuleByUID(ctx, &models.GetAlertRuleByUIDQuery{ - UID: query.RuleUID, - OrgID: query.OrgID, - }) - if err != nil { - return nil, fmt.Errorf("failed to fetch alert rule by UID: %w", err) - } - if rule == nil { - return nil, models.ErrAlertRuleNotFound - } - return nil, h.ac.AuthorizeAccessInFolder(ctx, query.SignedInUser, rule) - } - // if no filter, then we need to get all namespaces user has access to + + // All folders the user has access to. folders, err := h.ruleStore.GetUserVisibleNamespaces(ctx, query.OrgID, query.SignedInUser) if err != nil { return nil, fmt.Errorf("failed to fetch folders that user can access: %w", err) } uids := make([]string, 0, len(folders)) - // now keep only UIDs of folder in which user can read rules. + // Keep only UIDs of folder in which user can read rules. for _, f := range folders { - hasAccess, err := h.ac.HasAccessInFolder(ctx, query.SignedInUser, models.Namespace(*f.ToFolderReference())) + hasAccess, err := h.ac.HasAccessInFolder(ctx, query.SignedInUser, models.NewNamespace(f)) if err != nil { return nil, err } @@ -545,3 +588,71 @@ func (h *RemoteLokiBackend) getFolderUIDsForFilter(ctx context.Context, query mo sort.Strings(uids) return uids, nil } + +func (h *RemoteLokiBackend) getFolderUIDsForRuleFilter(ctx context.Context, query models.HistoryQuery, canReadAll bool) ([]string, error) { + rule, err := h.ruleStore.GetAlertRuleByUID(ctx, &models.GetAlertRuleByUIDQuery{ + UID: query.RuleUID, + OrgID: query.OrgID, + }) + if err != nil { + if canReadAll { + // When the user can read all rules, filtering by folder UID is purely an optimization, so we can ignore errors here. + h.log.FromContext(ctx).Debug("failed to fetch alert rule by UID", "err", err) + return nil, nil + } + return nil, fmt.Errorf("failed to fetch alert rule by UID: %w", err) + } + + // First, we check if the user has access to the current version of the rule. If not, we can return early. + // Whether we should check historical folders they might still have access to is not 100% clear, but it seems more + // intuitive to deny access in this case. + if !canReadAll { + if err := h.ac.AuthorizeAccessInFolder(ctx, query.SignedInUser, rule); err != nil { + return nil, err + } + } + + // We want to return folder UIDs when possible, as it's indexed in Loki and will help with query performance. + // However, by just returning the current folder UID the user can lose history when a rule is moved between folders. + // So, we attempt to get historical folder UIDs from the rule's history. + historicalFolders, err := h.ruleStore.GetAlertRuleVersionFolders(ctx, rule.OrgID, rule.GUID) + if err != nil { + // Including historical folders is an edge case enhancement, better to just log the error and continue + // with the current folder UID. + h.log.FromContext(ctx).Debug("failed to include historical folder UIDs for rule", "err", err) + } + + accessibleFolders := make([]string, 0, len(historicalFolders)+1) + dedup := make(map[string]struct{}) + + accessibleFolders = append(accessibleFolders, rule.GetNamespaceUID()) + dedup[rule.GetNamespaceUID()] = struct{}{} + + for _, folderUID := range historicalFolders { + if _, exists := dedup[folderUID]; exists { + continue + } + + if canReadAll { + // If the user can read all rules, no need to check access to each folder. + accessibleFolders = append(accessibleFolders, folderUID) + continue + } + + hasAccess, err := h.ac.HasAccessInFolder(ctx, query.SignedInUser, models.Namespace{ + UID: folderUID, + }) + if err != nil { + // Including historical folders is an edge case enhancement, better to just log the error and continue + // with the current folder UID. + h.log.FromContext(ctx).Debug("failed to check access to folder", "err", err, "folderUID", folderUID) + continue + } + if !hasAccess { + continue + } + accessibleFolders = append(accessibleFolders, folderUID) + } + + return accessibleFolders, nil +} diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index a01efe9563b..d5cd482d177 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -22,6 +22,7 @@ import ( alertingInstrument "github.com/grafana/alerting/http/instrument" "github.com/grafana/alerting/http/instrument/instrumenttest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -868,7 +869,8 @@ func TestGetFolderUIDsForFilter(t *testing.T) { } result, err := createLoki(ac).getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) assert.NoError(t, err) - assert.Empty(t, result) + assert.Len(t, result, 1) + assert.Contains(t, result, rule.GetNamespaceUID()) assert.Len(t, ac.Calls, 1) assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) @@ -893,7 +895,8 @@ func TestGetFolderUIDsForFilter(t *testing.T) { result, err := loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) assert.NoError(t, err) - assert.Empty(t, result) + assert.Len(t, result, 1) + assert.Contains(t, result, rule.GetNamespaceUID()) assert.Len(t, ac.Calls, 2) assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) @@ -916,6 +919,21 @@ func TestGetFolderUIDsForFilter(t *testing.T) { require.ErrorIs(t, err, models.ErrAlertRuleNotFound) }) }) + + t.Run("should return folderUID", func(t *testing.T) { + for _, authBypass := range []bool{true, false} { + t.Run(fmt.Sprintf("authBypass=%v", authBypass), func(t *testing.T) { + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return authBypass, nil + } + result, err := createLoki(ac).getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Contains(t, result, rule.GetNamespaceUID()) + }) + } + }) }) t.Run("when rule UID is empty", func(t *testing.T) { @@ -982,6 +1000,161 @@ func TestGetFolderUIDsForFilter(t *testing.T) { }) } +func TestGetFolderUIDsForFilterWithHistoricalFolders(t *testing.T) { + // Simple history generator to avoid repetitive code in test cases. + simpleHistory := func(guid string) map[string][]*models.AlertRuleVersion { + return map[string][]*models.AlertRuleVersion{ + guid: { + &models.AlertRuleVersion{AlertRule: models.RuleGen.With(models.RuleMuts.WithGUID(guid), models.RuleMuts.WithNamespaceUID("folder-current")).Generate()}, + &models.AlertRuleVersion{AlertRule: models.RuleGen.With(models.RuleMuts.WithGUID(guid), models.RuleMuts.WithNamespaceUID("folder-historical-1")).Generate()}, + &models.AlertRuleVersion{AlertRule: models.RuleGen.With(models.RuleMuts.WithGUID(guid), models.RuleMuts.WithNamespaceUID("folder-historical-2")).Generate()}, + &models.AlertRuleVersion{AlertRule: models.RuleGen.With(models.RuleMuts.WithGUID(guid), models.RuleMuts.WithNamespaceUID("folder-historical-3")).Generate()}, + }, + } + } + + // Helper to create simple folder access override functions. + canReadRulesInFolders := func(folderUids ...string) func(folderUID string) (bool, error) { + return func(folderUID string) (bool, error) { + for _, f := range folderUids { + if folderUID == f { + return true, nil + } + } + return false, nil + } + } + + // Helper to fail historical folders query. + failHistoryQueryHook := func(cmd any) error { + q, ok := cmd.(fakes.GenericRecordedQuery) + if !ok { + return nil + } + if q.Name == "GetAlertRuleVersionFolders" { + return errors.New("generic error") + } + return nil + } + + cases := []struct { + name string + // Setup. + existingHistory map[string][]*models.AlertRuleVersion + canReadAllRules bool + rule *models.AlertRule + + // Error overrides. + ruleStoreHook func(cmd any) error + folderAccessOverride func(folderUID string) (bool, error) + + // Expected. + expectedFolders []string + }{ + { + name: "should include historical folders when user can read all rules", + existingHistory: simpleHistory("guid-1"), + canReadAllRules: true, + rule: models.RuleGen.With(models.RuleMuts.WithGUID("guid-1"), models.RuleMuts.WithNamespaceUID("folder-current")).GenerateRef(), + expectedFolders: []string{"folder-current", "folder-historical-1", "folder-historical-2", "folder-historical-3"}, + }, + { + name: "should include only authorized historical folders", + existingHistory: simpleHistory("guid-1"), + folderAccessOverride: canReadRulesInFolders("folder-current", "folder-historical-2"), + rule: models.RuleGen.With(models.RuleMuts.WithGUID("guid-1"), models.RuleMuts.WithNamespaceUID("folder-current")).GenerateRef(), + expectedFolders: []string{"folder-current", "folder-historical-2"}, + }, + { + name: "if historical folders query fails, should return current folder", + existingHistory: simpleHistory("guid-1"), + canReadAllRules: true, + rule: models.RuleGen.With(models.RuleMuts.WithGUID("guid-1"), models.RuleMuts.WithNamespaceUID("folder-current")).GenerateRef(), + ruleStoreHook: failHistoryQueryHook, + expectedFolders: []string{"folder-current"}, + }, + { + name: "if historical folders query fails, should return current folder", + existingHistory: simpleHistory("guid-1"), + folderAccessOverride: canReadRulesInFolders("folder-current", "folder-historical-2"), + rule: models.RuleGen.With(models.RuleMuts.WithGUID("guid-1"), models.RuleMuts.WithNamespaceUID("folder-current")).GenerateRef(), + ruleStoreHook: failHistoryQueryHook, + expectedFolders: []string{"folder-current"}, + }, + { + name: "if access check for historical folders fails, should ignore", + existingHistory: simpleHistory("guid-1"), + rule: models.RuleGen.With(models.RuleMuts.WithGUID("guid-1"), models.RuleMuts.WithNamespaceUID("folder-current")).GenerateRef(), + folderAccessOverride: func(folderUID string) (bool, error) { + switch folderUID { + case "folder-current", "folder-historical-3": + return true, nil + case "folder-historical-2": + return false, nil + case "folder-historical-1": + return false, errors.New("generic error") + } + return false, nil + }, + expectedFolders: []string{"folder-current", "folder-historical-3"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Setup. + orgID := int64(1) + usr := accesscontrol.BackgroundUser("test", 1, org.RoleNone, nil) + + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return tc.canReadAllRules, nil + } + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error { + if tc.canReadAllRules { + return nil + } + hasAccess, err := tc.folderAccessOverride(namespaced.GetNamespaceUID()) + if err != nil { + return err + } + if !hasAccess { + return rulesAuthz.ErrAuthorizationBase.Errorf("%w", err) + } + return nil + } + ac.HasAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) (bool, error) { + if tc.canReadAllRules { + return true, nil + } + return tc.folderAccessOverride(namespaced.GetNamespaceUID()) + } + + rulesStore := fakes.NewRuleStore(t) + rulesStore.Rules = map[int64][]*models.AlertRule{ + orgID: { + tc.rule, + // Add some irrelevant rules to ensure they are ignored. + models.RuleGen.With(models.RuleMuts.WithNamespaceUID(tc.rule.GetNamespaceUID())).GenerateRef(), + models.RuleGen.With(models.RuleMuts.WithNamespaceUID("irrelevant-folder")).GenerateRef(), + }, + } + rulesStore.History = tc.existingHistory + if tc.ruleStoreHook != nil { + rulesStore.Hook = tc.ruleStoreHook + } + + loki := createTestLokiBackend(t, instrumenttest.NewFakeRequester(), metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + loki.ruleStore = rulesStore + loki.ac = ac + + // Test conditions. + result, err := loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: tc.rule.UID, SignedInUser: usr}) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedFolders, result) + }) + } +} + func createTestLokiBackend(t *testing.T, req alertingInstrument.Requester, met *metrics.Historian) *RemoteLokiBackend { url, _ := url.Parse("http://some.url") cfg := lokiclient.LokiConfig{ diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 401eeb079cf..45c29f9d684 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -184,8 +184,11 @@ func (st *Manager) Warm(ctx context.Context, orgReader OrgReader, rulesReader Ru continue } - // nil safety. - annotations := ruleForEntry.Annotations + // Use persisted annotations if available, otherwise fall back to rule annotations + annotations := entry.Annotations + if len(annotations) == 0 { + annotations = ruleForEntry.Annotations + } if annotations == nil { annotations = make(map[string]string) } @@ -229,9 +232,7 @@ func (st *Manager) Get(orgID int64, alertRuleUID string, stateId data.Fingerprin return st.cache.get(orgID, alertRuleUID, stateId) } -// DeleteStateByRuleUID removes the rule instances from cache and instanceStore. A closed channel is returned to be able -// to gracefully handle the clear state step in scheduler in case we do not need to use the historian to save state -// history. +// DeleteStateByRuleUID removes the rule instances from cache and instanceStore. func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKeyWithGroup, reason string) []StateTransition { logger := st.log.FromContext(ctx) logger.Debug("Resetting state of the rule") @@ -289,10 +290,14 @@ func (st *Manager) ForgetStateByRuleUID(ctx context.Context, ruleKey ngModels.Al // ResetStateByRuleUID removes the rule instances from cache and instanceStore and saves state history. If the state // history has to be saved, rule must not be nil. func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []StateTransition { + if rule == nil { + return nil + } + ruleKey := rule.GetKeyWithGroup() transitions := st.DeleteStateByRuleUID(ctx, ruleKey, reason) - if rule == nil || st.historian == nil || len(transitions) == 0 { + if st.historian == nil || len(transitions) == 0 { return transitions } diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 363d7aef78d..b8b366ac619 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -74,7 +74,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime), ResolvedAt: util.Pointer(evaluationTime), - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: rule.Annotations, // alert instance has no stored annotations, falls back to the rule annotations ResultFingerprint: data.Fingerprint(math.MaxUint64), }, { AlertRuleUID: rule.UID, @@ -87,7 +87,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, ResultFingerprint: data.Fingerprint(math.MaxUint64 - 1), }, { @@ -101,7 +101,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-3"}, ResultFingerprint: data.Fingerprint(0), }, { @@ -115,7 +115,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-4"}, ResultFingerprint: data.Fingerprint(1), }, { @@ -129,7 +129,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastEvaluationTime: evaluationTime, LastSentAt: nil, ResolvedAt: nil, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-5"}, ResultFingerprint: data.Fingerprint(2), }, } @@ -169,6 +169,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-2"}, ResultFingerprint: data.Fingerprint(math.MaxUint64 - 1).String(), }) @@ -187,6 +188,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-3"}, ResultFingerprint: data.Fingerprint(0).String(), }) @@ -205,6 +207,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-4"}, ResultFingerprint: data.Fingerprint(1).String(), }) @@ -223,6 +226,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: nil, ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-5"}, ResultFingerprint: data.Fingerprint(2).String(), }) @@ -241,6 +245,7 @@ func TestIntegrationWarmStateCache(t *testing.T) { LastSentAt: nil, ResolvedAt: nil, Labels: labels, + Annotations: models.InstanceAnnotations{"testAnnotation": "value-6"}, ResultFingerprint: data.Fingerprint(2).String(), }) @@ -1497,6 +1502,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { }, CurrentState: models.InstanceStateNormal, Labels: labels1, + Annotations: rule.Annotations, LastEvalTime: lastEval, CurrentStateSince: lastEval, CurrentStateEnd: lastEval.Add(3 * interval), @@ -1512,6 +1518,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { }, CurrentState: models.InstanceStateFiring, Labels: labels2, + Annotations: rule.Annotations, LastEvalTime: lastEval, CurrentStateSince: lastEval, CurrentStateEnd: lastEval.Add(3 * interval), @@ -1562,7 +1569,7 @@ func TestIntegrationStaleResultsHandler(t *testing.T) { LastSentAt: &lastEval, ResolvedAt: &lastEval, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: rule.Annotations, ResultFingerprint: data.Labels{"test1": "testValue1"}.Fingerprint(), }, }, @@ -1810,7 +1817,7 @@ func TestIntegrationDeleteStateByRuleUID(t *testing.T) { Labels: data.Labels{"test1": "testValue1"}, State: eval.Normal, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, { AlertRuleUID: rule.UID, @@ -1818,7 +1825,7 @@ func TestIntegrationDeleteStateByRuleUID(t *testing.T) { Labels: data.Labels{"test2": "testValue2"}, State: eval.Alerting, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, }, startingStateCacheCount: 2, @@ -1959,7 +1966,7 @@ func TestIntegrationResetStateByRuleUID(t *testing.T) { Labels: data.Labels{"test1": "testValue1"}, State: eval.Normal, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, { AlertRuleUID: rule.UID, @@ -1967,7 +1974,7 @@ func TestIntegrationResetStateByRuleUID(t *testing.T) { Labels: data.Labels{"test2": "testValue2"}, State: eval.Alerting, EvaluationDuration: 0, - Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, + Annotations: map[string]string{"testAnnotation": "value-2"}, }, }, startingStateCacheCount: 2, @@ -2044,6 +2051,39 @@ func TestIntegrationResetStateByRuleUID(t *testing.T) { } } +func TestResetStateByRuleUID(t *testing.T) { + ctx := context.Background() + + setupManager := func(historian state.Historian) *state.Manager { + cfg := state.ManagerCfg{ + Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), + ExternalURL: nil, + InstanceStore: &state.FakeInstanceStore{}, + Images: &state.NoopImageService{}, + Clock: clock.NewMock(), + Historian: historian, + Tracer: tracing.InitializeTracerForTest(), + Log: log.New("ngalert.state.manager"), + } + + return state.NewManager(cfg, state.NewNoopPersister()) + } + + t.Run("with nil historian", func(t *testing.T) { + manager := setupManager(nil) + + transitions := manager.ResetStateByRuleUID(ctx, nil, "test reason") + require.Empty(t, transitions) + }) + + t.Run("with historian", func(t *testing.T) { + manager := setupManager(&state.FakeHistorian{}) + + transitions := manager.ResetStateByRuleUID(ctx, nil, "test reason") + require.Empty(t, transitions) + }) +} + func setCacheID(s *state.State) *state.State { if s.CacheID != 0 { return s diff --git a/pkg/services/ngalert/state/persister_sync.go b/pkg/services/ngalert/state/persister_sync.go index 8a22afb9ca3..707afed7069 100644 --- a/pkg/services/ngalert/state/persister_sync.go +++ b/pkg/services/ngalert/state/persister_sync.go @@ -89,6 +89,7 @@ func (a *SyncStatePersister) saveAlertStates(ctx context.Context, states ...Stat instance := ngModels.AlertInstance{ AlertInstanceKey: key, Labels: ngModels.InstanceLabels(s.Labels), + Annotations: s.Annotations, CurrentState: ngModels.InstanceStateType(s.State.State.String()), CurrentReason: s.StateReason, LastEvalTime: s.LastEvaluationTime, diff --git a/pkg/services/ngalert/state/persister_sync_rule.go b/pkg/services/ngalert/state/persister_sync_rule.go index a237d2983f2..f2c2b69a2a7 100644 --- a/pkg/services/ngalert/state/persister_sync_rule.go +++ b/pkg/services/ngalert/state/persister_sync_rule.go @@ -90,6 +90,7 @@ func (a *SyncRuleStatePersister) Sync(ctx context.Context, span trace.Span, rule instance := models.AlertInstance{ AlertInstanceKey: key, Labels: models.InstanceLabels(s.Labels), + Annotations: s.Annotations, CurrentState: models.InstanceStateType(s.State.State.String()), CurrentReason: s.StateReason, LastEvalTime: s.LastEvaluationTime, diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 285a24d0b81..30856587794 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/prometheus/alertmanager/pkg/labels" "golang.org/x/exp/maps" "github.com/grafana/grafana/pkg/util/xorm" @@ -238,6 +239,27 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid st return alertRules, nil } +// GetAlertRuleVersionFolders retrieves a list of unique folder UIDs that the given rule guid has belonged to. +// Returned slice is ordered with more recent folders first. +func (st DBstore) GetAlertRuleVersionFolders(ctx context.Context, orgID int64, guid string) ([]string, error) { + folders := make([]string, 0) + err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + if err := sess.Table(new(alertRuleVersion)). + Select("rule_namespace_uid"). + Where("rule_org_id = ? AND rule_guid = ?", orgID, guid). + GroupBy("rule_namespace_uid"). + OrderBy("MAX(version) DESC"). + Find(&folders); err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + return folders, nil +} + // ListDeletedRules retrieves a list of deleted alert rules for the specified organization ID from the database. // It ensures that only the latest version of each rule is included and filters out invalid or duplicated versions. // Returns a slice of *models.AlertRule or an error if the operation fails. @@ -620,6 +642,23 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis _ = rows.Close() }() + opts := AlertRuleConvertOptions{} + if query.Compact { + opts.ExcludeAlertQueries = true + opts.ExcludeNotificationSettings = true + opts.ExcludeMetadata = true + + if query.ReceiverName != "" || query.TimeIntervalName != "" { + // Need NotificationSettings for these filters + opts.ExcludeNotificationSettings = false + } + + if query.HasPrometheusRuleDefinition != nil { + // Need Metadata for this filter + opts.ExcludeMetadata = false + } + } + // Process rules and implement per-group pagination var groupsFetched int64 var rulesFetched int64 @@ -631,7 +670,8 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis continue } - converted, err := alertRuleToModelsAlertRule(*rule, st.Logger) + converted, err := convertAlertRuleToModel(*rule, st.Logger, opts) + if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRulesByGroup", "error", err) continue @@ -796,6 +836,15 @@ func (st DBstore) ListAlertRulesPaginated(ctx context.Context, query *ngmodels.L return result, nextToken, err } +func matchersMatchLabels(matchers labels.Matchers, lbls map[string]string) bool { + for _, m := range matchers { + if !m.Matches(lbls[m.Name]) { + return false + } + } + return true +} + // nolint:gocyclo func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.ListAlertRulesExtendedQuery) (q *xorm.Session, groupsSet map[string]struct{}, err error) { q = sess.Table("alert_rule") @@ -914,6 +963,13 @@ func (st DBstore) buildListAlertRulesQuery(sess *db.Session, query *ngmodels.Lis } } + if len(query.LabelMatchers) > 0 { + q, err = st.filterByLabelMatchers(query.LabelMatchers, q) + if err != nil { + return nil, groupsSet, err + } + } + // FIXME: record is nullable but we don't save it as null when it's nil switch query.RuleType { case ngmodels.RuleTypeFilterAlerting: @@ -961,6 +1017,11 @@ func (st DBstore) handleRuleRow(rows *xorm.Rows, query *ngmodels.ListAlertRulesE return nil, false } } + if len(query.LabelMatchers) > 0 { // remove false-positive hits from the result + if !matchersMatchLabels(query.LabelMatchers, converted.Labels) { + return nil, false + } + } // MySQL (and potentially other databases) can use case-insensitive comparison. // This code makes sure we return groups that only exactly match the filter. if groupsSet != nil { @@ -1349,6 +1410,23 @@ func (st DBstore) filterWithPrometheusRuleDefinition(value bool, sess *xorm.Sess ), nil } +// filterByLabelMatchers adds filtering for equality and inequality label matchers. +// Returns error if regex matchers are passed. +func (st DBstore) filterByLabelMatchers(matchers labels.Matchers, sess *xorm.Session) (*xorm.Session, error) { + for _, m := range matchers { + if m.Type != labels.MatchEqual && m.Type != labels.MatchNotEqual { + return nil, fmt.Errorf("matcher %q %s %q is not supported", m.Name, m.Type, m.Value) + } + + sql, args, err := buildLabelMatcherCondition(st.SQLStore.GetDialect(), "labels", m) + if err != nil { + return nil, err + } + sess = sess.And(sql, args...) + } + return sess, nil +} + func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(ngmodels.Provenance) bool, dryRun bool) ([]ngmodels.AlertRuleKey, []ngmodels.AlertRuleKey, error) { // fetch entire rules because Update method requires it because it copies rules to version table rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{ diff --git a/pkg/services/ngalert/store/alert_rule_labels.go b/pkg/services/ngalert/store/alert_rule_labels.go new file mode 100644 index 00000000000..721071d5219 --- /dev/null +++ b/pkg/services/ngalert/store/alert_rule_labels.go @@ -0,0 +1,51 @@ +package store + +import ( + "fmt" + + "github.com/prometheus/alertmanager/pkg/labels" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +// buildLabelMatcherCondition builds SQL for a label matcher with Prometheus semantics. +// For MySQL/PostgreSQL, it uses JSON functions, and +// for SQLite, it uses GLOB patterns to find matching labels. +func buildLabelMatcherCondition(dialect migrator.Dialect, column string, m *labels.Matcher) (string, []any, error) { + if dialect.DriverName() == migrator.SQLite { + return buildLabelMatcherGlob(column, m) + } + return buildLabelMatcherJSON(dialect, column, m) +} + +func buildLabelMatcherGlob(column string, m *labels.Matcher) (string, []any, error) { + switch { + case m.Type == labels.MatchEqual && m.Value == "": + eqSQL, eqArgs, _ := globEquals(column, m.Name, "") + missingSQL, missingArgs, _ := globKeyMissing(column, m.Name) + return "(" + eqSQL + " OR " + missingSQL + ")", append(eqArgs, missingArgs...), nil + case m.Type == labels.MatchEqual: + return globEquals(column, m.Name, m.Value) + case m.Type == labels.MatchNotEqual: + return globNotEquals(column, m.Name, m.Value) + default: + return "", nil, fmt.Errorf("unsupported matcher type: %v", m.Type) + } +} + +func buildLabelMatcherJSON(dialect migrator.Dialect, column string, m *labels.Matcher) (string, []any, error) { + switch { + case m.Type == labels.MatchEqual && m.Value == "": + eqSQL, eqArgs := jsonEquals(dialect, column, m.Name, "") + missingSQL, missingArgs := jsonKeyMissing(dialect, column, m.Name) + return "(" + eqSQL + " OR " + missingSQL + ")", append(eqArgs, missingArgs...), nil + case m.Type == labels.MatchEqual: + sql, args := jsonEquals(dialect, column, m.Name, m.Value) + return sql, args, nil + case m.Type == labels.MatchNotEqual: + sql, args := jsonNotEquals(dialect, column, m.Name, m.Value) + return sql, args, nil + default: + return "", nil, fmt.Errorf("unsupported matcher type: %v", m.Type) + } +} diff --git a/pkg/services/ngalert/store/alert_rule_labels_test.go b/pkg/services/ngalert/store/alert_rule_labels_test.go new file mode 100644 index 00000000000..2006c49fd7a --- /dev/null +++ b/pkg/services/ngalert/store/alert_rule_labels_test.go @@ -0,0 +1,136 @@ +package store + +import ( + "testing" + + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +func TestBuildLabelMatcherGlob(t *testing.T) { + tests := []struct { + name string + matcher *labels.Matcher + wantSQL string + wantArgs []any + wantErr bool + errContains string + }{ + { + name: "MatchEqual with non-empty value", + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: "alerting"}, + wantSQL: "labels GLOB ?", + wantArgs: []any{`*"team":"alerting"*`}, + }, + { + name: "MatchEqual with empty value (Prometheus semantics)", + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: ""}, + wantSQL: `(labels GLOB ? OR labels NOT GLOB ?)`, + wantArgs: []any{`*"team":""*`, `*"team":*`}, + }, + { + name: "MatchNotEqual", + matcher: &labels.Matcher{Type: labels.MatchNotEqual, Name: "team", Value: "alerting"}, + wantSQL: "labels NOT GLOB ?", + wantArgs: []any{`*"team":"alerting"*`}, + }, + { + name: "unsupported matcher type", + matcher: &labels.Matcher{Type: labels.MatchRegexp, Name: "team", Value: "alert.*"}, + wantErr: true, + errContains: "unsupported matcher type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, args, err := buildLabelMatcherGlob("labels", tt.matcher) + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errContains) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantSQL, sql) + require.Equal(t, tt.wantArgs, args) + }) + } +} + +func TestBuildLabelMatcherJSON(t *testing.T) { + tests := []struct { + name string + dialect migrator.Dialect + matcher *labels.Matcher + wantSQL string + wantArgs []any + wantErr bool + errContains string + }{ + { + name: "MySQL MatchEqual with non-empty value", + dialect: migrator.NewMysqlDialect(), + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: "alerting"}, + wantSQL: "JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ?", + wantArgs: []any{"team", "alerting"}, + }, + { + name: "MySQL MatchEqual with empty value", + dialect: migrator.NewMysqlDialect(), + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: ""}, + wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ? OR JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NULL)", + wantArgs: []any{"team", "", "team"}, + }, + { + name: "MySQL MatchNotEqual", + dialect: migrator.NewMysqlDialect(), + matcher: &labels.Matcher{Type: labels.MatchNotEqual, Name: "team", Value: "alerting"}, + wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) != ?)", + wantArgs: []any{"team", "team", "alerting"}, + }, + { + name: "PostgreSQL MatchEqual with non-empty value", + dialect: migrator.NewPostgresDialect(), + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: "alerting"}, + wantSQL: "jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) = ?", + wantArgs: []any{"team", "alerting"}, + }, + { + name: "PostgreSQL MatchEqual with empty value", + dialect: migrator.NewPostgresDialect(), + matcher: &labels.Matcher{Type: labels.MatchEqual, Name: "team", Value: ""}, + wantSQL: "(jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) = ? OR jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) IS NULL)", + wantArgs: []any{"team", "", "team"}, + }, + { + name: "PostgreSQL MatchNotEqual", + dialect: migrator.NewPostgresDialect(), + matcher: &labels.Matcher{Type: labels.MatchNotEqual, Name: "team", Value: "alerting"}, + wantSQL: "(jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) IS NULL OR jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) != ?)", + wantArgs: []any{"team", "team", "alerting"}, + }, + { + name: "unsupported matcher type", + dialect: migrator.NewMysqlDialect(), + matcher: &labels.Matcher{Type: labels.MatchRegexp, Name: "team", Value: "alert.*"}, + wantErr: true, + errContains: "unsupported matcher type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, args, err := buildLabelMatcherJSON(tt.dialect, "labels", tt.matcher) + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errContains) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantSQL, sql) + require.Equal(t, tt.wantArgs, args) + }) + } +} diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 93f6f9e57a4..f7497c7b05a 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -13,6 +13,7 @@ import ( "github.com/benbjohnson/clock" "github.com/google/uuid" + "github.com/prometheus/alertmanager/pkg/labels" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1726,6 +1727,55 @@ func TestIntegrationGetRuleVersions(t *testing.T) { }) } +func TestIntegrationGetAlertRuleVersionFolders(t *testing.T) { + tutil.SkipIntegrationTestInShortMode(t) + + // Setup. + cfg := setting.NewCfg() + cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int64N(100)+1) * time.Second} + sqlStore := db.InitTestDB(t) + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) + orgID := int64(1) + gen := models.RuleGen + gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID), gen.WithVersion(1)) + + inserted, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.InsertRule{{AlertRule: gen.Generate()}}) + require.NoError(t, err) + ruleV1, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: inserted[0].UID}) + require.NoError(t, err) + + oldRule := ruleV1 + updatedRule := ruleV1 + updateRule := func(title string, folderUID string) { + oldRule = updatedRule + updatedRule = models.CopyRule(oldRule, gen.WithTitle(title), gen.WithNamespaceUID(folderUID)) + require.NoError(t, store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, []models.UpdateRule{{Existing: oldRule, New: *updatedRule}})) + updatedRule.Version++ // Simulate version increment after update to avoid conflict errors. + } + + // Update rule a couple of times to create versions. + originalFolder := oldRule.NamespaceUID + updateRule(util.GenerateShortUID(), originalFolder) + updateRule(util.GenerateShortUID(), "newfolder-1") + updateRule(util.GenerateShortUID(), "newfolder-2") + updateRule(util.GenerateShortUID(), "newfolder-2") + updateRule(util.GenerateShortUID(), originalFolder) + updateRule(util.GenerateShortUID(), "current-folder") + + t.Run("should return rule versions folders sorted in decreasing order", func(t *testing.T) { + historicalFolders, err := store.GetAlertRuleVersionFolders(context.Background(), updatedRule.OrgID, updatedRule.GUID) + require.NoError(t, err) + assert.Equal(t, []string{ // Return folders with more recent first. + "current-folder", + originalFolder, + "newfolder-2", + "newfolder-1", + }, historicalFolders) + }) +} + // createAlertRule creates an alert rule in the database and returns it. // If a generator is not specified, uniqueness of primary key is not guaranteed. func createRule(tb testing.TB, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule { @@ -2385,6 +2435,163 @@ func TestIntegration_ListAlertRules(t *testing.T) { }) } }) + + t.Run("filter by LabelMatchers", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) + + ruleLower := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"team": "alerting", "severity": "warning"}), + ruleGen.WithTitle("rule_lowercase"))) + ruleUpper := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"team": "Alerting", "severity": "critical"}), + ruleGen.WithTitle("rule_uppercase"))) + ruleSpecial := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"key": `value"with"quotes`}), + ruleGen.WithTitle("rule_special"))) + ruleGlob := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"glob": "*[?]"}), + ruleGen.WithTitle("rule_glob"))) + ruleSpecialChars := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"json": "line1\nline2\\end\"quote"}), + ruleGen.WithTitle("rule_special_chars"))) + ruleEmpty := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"empty": ""}), + ruleGen.WithTitle("rule_empty"))) + ruleNonempty := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{"empty": "nonempty"}), + ruleGen.WithTitle("rule_nonempty"))) + // include a rule with no labels at all, + // to ensure we handle that case correctly. + // JSON functions need to be able to handle null and empty string values. + ruleNoLabels := createRule(t, store, ruleGen.With( + ruleGen.WithLabels(map[string]string{}), + ruleGen.WithTitle("rule_no_labels"))) + + tc := []struct { + name string + labelMatchers labels.Matchers + expectedRules []*models.AlertRule + }{ + { + name: "equality matcher is case-sensitive", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "team", "alerting"); return m }(), + }, + expectedRules: []*models.AlertRule{ruleLower}, + }, + { + name: "equality matcher matches uppercase when specified", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "team", "Alerting"); return m }(), + }, + expectedRules: []*models.AlertRule{ruleUpper}, + }, + { + name: "inequality matcher is case-sensitive", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchNotEqual, "team", "alerting"); return m }(), + }, + expectedRules: []*models.AlertRule{ruleUpper, ruleSpecial, ruleGlob, ruleSpecialChars, ruleEmpty, ruleNonempty, ruleNoLabels}, + }, + { + name: "special characters in labels are handled correctly", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { + m, _ := labels.NewMatcher(labels.MatchEqual, "key", `value"with"quotes`) + return m + }(), + }, + expectedRules: []*models.AlertRule{ruleSpecial}, + }, + { + name: "matcher with non-existent label returns no rules", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "nonexistent", "value"); return m }(), + }, + expectedRules: []*models.AlertRule{}, + }, + { + name: "multiple matchers are ANDed", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "team", "Alerting"); return m }(), + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "severity", "critical"); return m }(), + }, + expectedRules: []*models.AlertRule{ruleUpper}, + }, + { + name: "GLOB special characters are escaped correctly", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "glob", "*[?]"); return m }(), + }, + expectedRules: []*models.AlertRule{ruleGlob}, + }, + { + name: "JSON escape characters are handled correctly", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { + m, _ := labels.NewMatcher(labels.MatchEqual, "json", "line1\nline2\\end\"quote") + return m + }(), + }, + expectedRules: []*models.AlertRule{ruleSpecialChars}, + }, + { + name: "empty string value matches correctly", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchEqual, "empty", ""); return m }(), + }, + expectedRules: []*models.AlertRule{ruleLower, ruleUpper, ruleSpecial, ruleGlob, ruleSpecialChars, ruleEmpty, ruleNoLabels}, + }, + { + name: "inequality matcher on non-existent label matches all rules", + labelMatchers: labels.Matchers{ + func() *labels.Matcher { + m, _ := labels.NewMatcher(labels.MatchNotEqual, "nonexistent", "value") + return m + }(), + }, + expectedRules: []*models.AlertRule{ruleLower, ruleUpper, ruleSpecial, ruleGlob, ruleSpecialChars, ruleEmpty, ruleNonempty, ruleNoLabels}, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + query := &models.ListAlertRulesQuery{ + OrgID: orgID, + LabelMatchers: tt.labelMatchers, + } + result, err := store.ListAlertRules(context.Background(), query) + require.NoError(t, err) + require.ElementsMatch(t, tt.expectedRules, result) + }) + } + + t.Run("regex matcher returns error from store", func(t *testing.T) { + query := &models.ListAlertRulesQuery{ + OrgID: orgID, + LabelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchRegexp, "team", "alert.*"); return m }(), + }, + } + _, err := store.ListAlertRules(context.Background(), query) + require.Error(t, err) + require.ErrorContains(t, err, "is not supported") + }) + + t.Run("not-regex matcher returns error from store", func(t *testing.T) { + query := &models.ListAlertRulesQuery{ + OrgID: orgID, + LabelMatchers: labels.Matchers{ + func() *labels.Matcher { m, _ := labels.NewMatcher(labels.MatchNotRegexp, "team", "alert.*"); return m }(), + }, + } + _, err := store.ListAlertRules(context.Background(), query) + require.Error(t, err) + require.ErrorContains(t, err, "is not supported") + }) + }) } func TestIntegration_ListAlertRulesPaginated(t *testing.T) { diff --git a/pkg/services/ngalert/store/compat.go b/pkg/services/ngalert/store/compat.go index 4f4194facc1..57dcf411853 100644 --- a/pkg/services/ngalert/store/compat.go +++ b/pkg/services/ngalert/store/compat.go @@ -10,11 +10,39 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) +// We only care about the data source UIDs. +type compactQuery struct { + DatasourceUID string `json:"datasourceUid"` +} + +// AlertRuleConvertOptions controls which fields to parse during conversion from alertRule to models.AlertRule. +// By default all fields are included. Set Exclude* to true to skip parsing expensive fields. +type AlertRuleConvertOptions struct { + ExcludeAlertQueries bool // Only parse datasource UIDs from queries + ExcludeNotificationSettings bool + ExcludeMetadata bool +} + func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) { + return convertAlertRuleToModel(ar, l, AlertRuleConvertOptions{}) +} + +// convertAlertRuleToModel creates a models.AlertRule from an alertRule. +// opts.Exclude* fields control which expensive fields to skip parsing, reducing JSON serializations. +func convertAlertRuleToModel(ar alertRule, l log.Logger, opts AlertRuleConvertOptions) (models.AlertRule, error) { var data []models.AlertQuery - err := json.Unmarshal([]byte(ar.Data), &data) - if err != nil { - return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err) + if opts.ExcludeAlertQueries { + var cqs []compactQuery + if err := json.Unmarshal([]byte(ar.Data), &cqs); err != nil { + return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err) + } + for _, cq := range cqs { + data = append(data, models.AlertQuery{DatasourceUID: cq.DatasourceUID}) + } + } else { + if err := json.Unmarshal([]byte(ar.Data), &data); err != nil { + return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err) + } } result := models.AlertRule{ @@ -52,6 +80,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e result.UpdatedBy = util.Pointer(models.UserUID(*ar.UpdatedBy)) } + var err error if ar.NoDataState != "" { result.NoDataState, err = models.NoDataStateFromString(ar.NoDataState) if err != nil { @@ -90,7 +119,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e } } - if ar.NotificationSettings != "" { + if !opts.ExcludeNotificationSettings && ar.NotificationSettings != "" { ns, err := parseNotificationSettings(ar.NotificationSettings) if err != nil { return models.AlertRule{}, fmt.Errorf("failed to parse notification settings: %w", err) @@ -98,7 +127,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e result.NotificationSettings = ns } - if ar.Metadata != "" { + if !opts.ExcludeMetadata && ar.Metadata != "" { err = json.Unmarshal([]byte(ar.Metadata), &result.Metadata) if err != nil { return models.AlertRule{}, fmt.Errorf("failed to metadata: %w", err) diff --git a/pkg/services/ngalert/store/compat_test.go b/pkg/services/ngalert/store/compat_test.go index 990ec5dd3e1..d4aab40931f 100644 --- a/pkg/services/ngalert/store/compat_test.go +++ b/pkg/services/ngalert/store/compat_test.go @@ -65,6 +65,165 @@ func TestAlertRuleToModelsAlertRule(t *testing.T) { }) } +func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) { + t.Run("should only extract datasource UIDs in compact mode", func(t *testing.T) { + rule := alertRule{ + ID: 1, + OrgID: 1, + UID: "test-uid", + Title: "Test Rule", + Condition: "A", + Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}},{"datasourceUid":"ds2","refId":"B","queryType":"test","model":{"expr":"down"}}]`, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: "ns-uid", + RuleGroup: "test-group", + NoDataState: "NoData", + ExecErrState: "Error", + NotificationSettings: `[{"receiver":"test-receiver"}]`, + Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`, + } + + compactResult, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: true, + ExcludeMetadata: true, + }) + require.NoError(t, err) + + // Should have datasource UIDs. + require.Len(t, compactResult.Data, 2) + require.Equal(t, "ds1", compactResult.Data[0].DatasourceUID) + require.Equal(t, "ds2", compactResult.Data[1].DatasourceUID) + + // But should not have full query data (RefID, QueryType, Model should be empty). + require.Empty(t, compactResult.Data[0].RefID) + require.Empty(t, compactResult.Data[0].QueryType) + require.Nil(t, compactResult.Data[0].Model) + require.Empty(t, compactResult.Data[1].RefID) + require.Empty(t, compactResult.Data[1].QueryType) + require.Nil(t, compactResult.Data[1].Model) + + // Should not have notification settings. + require.Empty(t, compactResult.NotificationSettings) + + // Should not have metadata (should be zero value). + require.Equal(t, ngmodels.AlertRuleMetadata{}, compactResult.Metadata) + }) + + t.Run("should parse full data in non-compact mode", func(t *testing.T) { + rule := alertRule{ + ID: 1, + OrgID: 1, + UID: "test-uid", + Title: "Test Rule", + Condition: "A", + Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}},{"datasourceUid":"ds2","refId":"B","queryType":"test","model":{"expr":"down"}}]`, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: "ns-uid", + RuleGroup: "test-group", + NoDataState: "NoData", + ExecErrState: "Error", + NotificationSettings: `[{"receiver":"test-receiver"}]`, + Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`, + } + + fullResult, err := alertRuleToModelsAlertRule(rule, &logtest.Fake{}) + require.NoError(t, err) + + // Should have full query data. + require.Len(t, fullResult.Data, 2) + require.Equal(t, "ds1", fullResult.Data[0].DatasourceUID) + require.Equal(t, "A", fullResult.Data[0].RefID) + require.Equal(t, "test", fullResult.Data[0].QueryType) + require.NotNil(t, fullResult.Data[0].Model) + + // Should have notification settings. + require.Len(t, fullResult.NotificationSettings, 1) + require.Equal(t, "test-receiver", fullResult.NotificationSettings[0].Receiver) + + // Should have metadata (metadata is parsed from JSON to struct). + require.NotEqual(t, ngmodels.AlertRuleMetadata{}, fullResult.Metadata) + }) + + t.Run("compact mode with notification settings included for filtering", func(t *testing.T) { + rule := alertRule{ + ID: 1, + OrgID: 1, + UID: "test-uid", + Title: "Test Rule", + Condition: "A", + Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}}]`, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: "ns-uid", + RuleGroup: "test-group", + NoDataState: "NoData", + ExecErrState: "Error", + NotificationSettings: `[{"receiver":"test-receiver"}]`, + Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`, + } + + result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: false, + ExcludeMetadata: true, + }) + require.NoError(t, err) + + // Should have compact query data (only datasource UIDs). + require.Len(t, result.Data, 1) + require.Equal(t, "ds1", result.Data[0].DatasourceUID) + require.Empty(t, result.Data[0].RefID) + + // Should have notification settings for filtering. + require.Len(t, result.NotificationSettings, 1) + require.Equal(t, "test-receiver", result.NotificationSettings[0].Receiver) + + // Should not have metadata. + require.Equal(t, ngmodels.AlertRuleMetadata{}, result.Metadata) + }) + + t.Run("compact mode with metadata included for filtering", func(t *testing.T) { + rule := alertRule{ + ID: 1, + OrgID: 1, + UID: "test-uid", + Title: "Test Rule", + Condition: "A", + Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}}]`, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: "ns-uid", + RuleGroup: "test-group", + NoDataState: "NoData", + ExecErrState: "Error", + NotificationSettings: `[{"receiver":"test-receiver"}]`, + Metadata: `{"prometheus_style_rule":{"original_rule_definition":"alert: TestAlert\n expr: rate(metric[5m]) > 1"}}`, + } + + result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: true, + ExcludeMetadata: false, + }) + require.NoError(t, err) + + // Should have compact query data (only datasource UIDs). + require.Len(t, result.Data, 1) + require.Equal(t, "ds1", result.Data[0].DatasourceUID) + require.Empty(t, result.Data[0].RefID) + + // Should not have notification settings. + require.Empty(t, result.NotificationSettings) + + // Should have metadata for filtering. + require.NotEqual(t, ngmodels.AlertRuleMetadata{}, result.Metadata) + require.True(t, result.HasPrometheusRuleDefinition()) + }) +} + func TestAlertRuleVersionToAlertRule(t *testing.T) { g := ngmodels.RuleGen diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go index 44822f72e99..cc385ef4c63 100644 --- a/pkg/services/ngalert/store/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -99,6 +99,10 @@ func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance m if err != nil { return err } + annotationsJSON, err := alertInstance.Annotations.ToDB() + if err != nil { + return err + } params := append(make([]any, 0), alertInstance.RuleOrgID, alertInstance.RuleUID, @@ -113,12 +117,13 @@ func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance m nullableTimeToUnix(alertInstance.ResolvedAt), nullableTimeToUnix(alertInstance.LastSentAt), alertInstance.ResultFingerprint, + annotationsJSON, ) upsertSQL := st.SQLStore.GetDialect().UpsertSQL( "alert_instance", []string{"rule_org_id", "rule_uid", "labels_hash"}, - []string{"rule_org_id", "rule_uid", "labels", "labels_hash", "current_state", "current_reason", "current_state_since", "current_state_end", "last_eval_time", "fired_at", "resolved_at", "last_sent_at", "result_fingerprint"}) + []string{"rule_org_id", "rule_uid", "labels", "labels_hash", "current_state", "current_reason", "current_state_since", "current_state_end", "last_eval_time", "fired_at", "resolved_at", "last_sent_at", "result_fingerprint", "annotations"}) _, err = sess.SQL(upsertSQL, params...).Query() if err != nil { return err @@ -359,10 +364,10 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ query := strings.Builder{} placeholders := make([]string, 0, len(batch)) - args := make([]any, 0, len(batch)*12) + args := make([]any, 0, len(batch)*14) query.WriteString("INSERT INTO alert_instance ") - query.WriteString("(rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, fired_at, resolved_at, last_sent_at) VALUES ") + query.WriteString("(rule_org_id, rule_uid, labels, labels_hash, current_state, current_reason, current_state_since, current_state_end, last_eval_time, fired_at, resolved_at, last_sent_at, result_fingerprint, annotations) VALUES ") for _, instance := range batch { if err := models.ValidateAlertInstance(instance); err != nil { @@ -376,7 +381,13 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ continue } - placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?,?,?)") + annotationsJSON, err := instance.Annotations.ToDB() + if err != nil { + st.Logger.Warn("Skipping instance with invalid annotations", "err", err, "rule_uid", instance.RuleUID) + continue + } + + placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?,?,?,?,?)") args = append(args, instance.RuleOrgID, instance.RuleUID, @@ -390,6 +401,8 @@ func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch [ nullableTimeToUnix(instance.FiredAt), nullableTimeToUnix(instance.ResolvedAt), nullableTimeToUnix(instance.LastSentAt), + instance.ResultFingerprint, + annotationsJSON, ) } diff --git a/pkg/services/ngalert/store/instance_database_test.go b/pkg/services/ngalert/store/instance_database_test.go index ce0d9c795da..4f833d0fd17 100644 --- a/pkg/services/ngalert/store/instance_database_test.go +++ b/pkg/services/ngalert/store/instance_database_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/golang/snappy" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/grafana/pkg/util/testutil" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -17,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" pb "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1" "github.com/grafana/grafana/pkg/services/ngalert/tests" - "github.com/grafana/grafana/pkg/util" ) const baseIntervalSeconds = 10 @@ -51,7 +52,15 @@ func TestIntegration_CompressedAlertRuleStateOperations(t *testing.T) { name: "can save and read alert rule state", setupInstances: func() []models.AlertInstance { return []models.AlertInstance{ - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "labelsHash1", string(models.InstanceStateError), models.InstanceStateFiring), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("labelsHash1"), + models.InstanceMuts.WithReason(string(models.InstanceStateError)), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } }, listQuery: &models.ListAlertInstancesQuery{ @@ -67,8 +76,22 @@ func TestIntegration_CompressedAlertRuleStateOperations(t *testing.T) { name: "can save and read alert rule state with multiple instances", setupInstances: func() []models.AlertInstance { return []models.AlertInstance{ - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash1", "", models.InstanceStateFiring), - createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash2", "", models.InstanceStateFiring), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("hash1"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule1.OrgID), + models.InstanceMuts.WithRuleUID(alertRule1.UID), + models.InstanceMuts.WithLabelsHash("hash2"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } }, listQuery: &models.ListAlertInstancesQuery{ @@ -109,19 +132,6 @@ func containsHash(t *testing.T, instances []*models.AlertInstance, hash string) require.Fail(t, fmt.Sprintf("%v does not contain an instance with hash %s", instances, hash)) } -func createAlertInstance(orgID int64, ruleUID, labelsHash, reason string, state models.InstanceStateType) models.AlertInstance { - return models.AlertInstance{ - AlertInstanceKey: models.AlertInstanceKey{ - RuleOrgID: orgID, - RuleUID: ruleUID, - LabelsHash: labelsHash, - }, - CurrentState: state, - CurrentReason: reason, - Labels: models.InstanceLabels{"label1": "value1"}, - } -} - func TestIntegrationAlertInstanceOperations(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -312,7 +322,10 @@ func TestIntegrationFullSync(t *testing.T) { instances := make([]models.AlertInstance, len(ruleUIDs)) for i, ruleUID := range ruleUIDs { - instances[i] = generateTestAlertInstance(orgID, ruleUID) + instances[i] = *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(orgID), + models.InstanceMuts.WithRuleUID(ruleUID), + ) } t.Run("Should do a proper full sync", func(t *testing.T) { @@ -356,7 +369,7 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should add new entries on sync", func(t *testing.T) { newRuleUID := "y" - err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize, nil) + err := ng.InstanceStore.FullSync(ctx, append(instances, *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(newRuleUID))), batchSize, nil) require.NoError(t, err) res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ @@ -380,34 +393,40 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should save all instances when batch size is bigger than 1", func(t *testing.T) { batchSize = 2 - newRuleUID := "y" - err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize, nil) + testInstances := []models.AlertInstance{ + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch1"), models.InstanceMuts.WithResultFingerprint("fp0")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch2"), models.InstanceMuts.WithResultFingerprint("fp1")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch3"), models.InstanceMuts.WithResultFingerprint("fp2")), + } + + err := ng.InstanceStore.FullSync(ctx, testInstances, batchSize, nil) require.NoError(t, err) res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) - require.Len(t, res, len(instances)+1) - for _, ruleUID := range append(ruleUIDs, newRuleUID) { - found := false - for _, instance := range res { - if instance.RuleUID == ruleUID { - found = true - continue - } - } - if !found { - t.Errorf("Instance with RuleUID '%s' not found", ruleUID) - } + + savedInstances := make([]models.AlertInstance, len(res)) + for i, r := range res { + savedInstances[i] = *r } + + opts := []cmp.Option{ + cmpopts.EquateApproxTime(time.Second), // we don't get the same precision back from the DB + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b models.AlertInstance) bool { + return a.RuleUID < b.RuleUID + }), + } + require.Empty(t, cmp.Diff(testInstances, savedInstances, opts...)) }) t.Run("Should not fail when the instances are empty", func(t *testing.T) { // First, insert some data into the table. initialInstances := []models.AlertInstance{ - generateTestAlertInstance(orgID, "preexisting-1"), - generateTestAlertInstance(orgID, "preexisting-2"), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("preexisting-1")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("preexisting-2")), } err := ng.InstanceStore.FullSync(ctx, initialInstances, 5, nil) require.NoError(t, err) @@ -439,9 +458,9 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should handle invalid instances by skipping them", func(t *testing.T) { // Create a batch with one valid and one invalid instance - validInstance := generateTestAlertInstance(orgID, "valid") + validInstance := *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("valid")) - invalidInstance := generateTestAlertInstance(orgID, "") + invalidInstance := *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("")) // Make the invalid instance actually invalid invalidInstance.RuleUID = "" @@ -460,8 +479,8 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should handle batchSize larger than the number of instances", func(t *testing.T) { // Insert a small number of instances but use a large batchSize smallSet := []models.AlertInstance{ - generateTestAlertInstance(orgID, "batch-test1"), - generateTestAlertInstance(orgID, "batch-test2"), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch-test1")), + *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID("batch-test2")), } err := ng.InstanceStore.FullSync(ctx, smallSet, 100, nil) @@ -493,7 +512,7 @@ func TestIntegrationFullSync(t *testing.T) { largeCount := 300 largeSet := make([]models.AlertInstance, largeCount) for i := 0; i < largeCount; i++ { - largeSet[i] = generateTestAlertInstance(orgID, fmt.Sprintf("large-%d", i)) + largeSet[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("large-%d", i))) } err = ng.InstanceStore.FullSync(ctx, largeSet, 50, nil) @@ -520,7 +539,10 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { instances := make([]models.AlertInstance, len(ruleUIDs)) for i, ruleUID := range ruleUIDs { - instances[i] = generateTestAlertInstance(orgID, ruleUID) + instances[i] = *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(orgID), + models.InstanceMuts.WithRuleUID(ruleUID), + ) } // Simple jitter function for testing @@ -565,7 +587,7 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { t.Run("Should handle zero delays (immediate execution)", func(t *testing.T) { testInstances := make([]models.AlertInstance, 2) for i := 0; i < 2; i++ { - testInstances[i] = generateTestAlertInstance(orgID, fmt.Sprintf("immediate-%d", i)) + testInstances[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("immediate-%d", i))) } // Function that returns zero delays @@ -592,7 +614,7 @@ func TestIntegrationFullSyncWithJitter(t *testing.T) { t.Run("Should execute jitter delays correctly and save data", func(t *testing.T) { testInstances := make([]models.AlertInstance, 4) for i := 0; i < 4; i++ { - testInstances[i] = generateTestAlertInstance(orgID, fmt.Sprintf("jitter-test-%d", i)) + testInstances[i] = *models.AlertInstanceGen(models.InstanceMuts.WithOrgID(orgID), models.InstanceMuts.WithRuleUID(fmt.Sprintf("jitter-test-%d", i))) } // Track jitter function calls @@ -652,11 +674,16 @@ func TestIntegration_ProtoInstanceDBStore_VerifyCompressedData(t *testing.T) { alertRule := tests.CreateTestAlertRule(t, ctx, dbstore, 60, 1) - labelsHash := "hash1" - reason := "reason" - state := models.InstanceStateFiring instances := []models.AlertInstance{ - createAlertInstance(alertRule.OrgID, alertRule.UID, labelsHash, reason, state), + *models.AlertInstanceGen( + models.InstanceMuts.WithOrgID(alertRule.OrgID), + models.InstanceMuts.WithRuleUID(alertRule.UID), + models.InstanceMuts.WithLabelsHash("hash1"), + models.InstanceMuts.WithReason("reason"), + models.InstanceMuts.WithState(models.InstanceStateFiring), + models.InstanceMuts.WithLabels(models.InstanceLabels{"label1": "value1"}), + models.InstanceMuts.WithAnnotations(models.InstanceAnnotations{"annotation1": "value1"}), + ), } err := ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule.GetKeyWithGroup(), instances) @@ -704,25 +731,3 @@ func decompressAlertInstances(compressed []byte) ([]*pb.AlertInstance, error) { return instances.Instances, nil } - -func generateTestAlertInstance(orgID int64, ruleID string) models.AlertInstance { - return models.AlertInstance{ - AlertInstanceKey: models.AlertInstanceKey{ - RuleOrgID: orgID, - RuleUID: ruleID, - LabelsHash: "abc", - }, - CurrentState: models.InstanceStateFiring, - Labels: map[string]string{ - "hello": "world", - }, - ResultFingerprint: "abc", - CurrentStateEnd: time.Now(), - CurrentStateSince: time.Now(), - LastEvalTime: time.Now(), - LastSentAt: util.Pointer(time.Now()), - FiredAt: util.Pointer(time.Now()), - ResolvedAt: util.Pointer(time.Now()), - CurrentReason: "abc", - } -} diff --git a/pkg/services/ngalert/store/json.go b/pkg/services/ngalert/store/json.go new file mode 100644 index 00000000000..e0c71975a76 --- /dev/null +++ b/pkg/services/ngalert/store/json.go @@ -0,0 +1,101 @@ +package store + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +// JSON functions for MySQL/PostgreSQL + +func jsonEquals(dialect migrator.Dialect, column, key, value string) (string, []any) { + switch dialect.DriverName() { + case migrator.MySQL: + return fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?))) = ?", column), []any{key, value} + case migrator.Postgres: + return fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?) = ?", column), []any{key, value} + default: + return "", nil + } +} + +func jsonNotEquals(dialect migrator.Dialect, column, key, value string) (string, []any) { + var jx string + switch dialect.DriverName() { + case migrator.MySQL: + jx = fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?)))", column) + case migrator.Postgres: + jx = fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?)", column) + default: + return "", nil + } + return fmt.Sprintf("(%s IS NULL OR %s != ?)", jx, jx), []any{key, key, value} +} + +func jsonKeyMissing(dialect migrator.Dialect, column, key string) (string, []any) { + switch dialect.DriverName() { + case migrator.MySQL: + return fmt.Sprintf("JSON_EXTRACT(NULLIF(%s, ''), CONCAT('$.', ?)) IS NULL", column), []any{key} + case migrator.Postgres: + return fmt.Sprintf("jsonb_extract_path_text(NULLIF(%s, '')::jsonb, ?) IS NULL", column), []any{key} + default: + return "", nil + } +} + +// GLOB functions for SQLite + +func globEquals(column, key, value string) (string, []any, error) { + pattern, err := buildGlobPattern(key, value) + if err != nil { + return "", nil, err + } + return column + " GLOB ?", []any{"*" + pattern + "*"}, nil +} + +func globNotEquals(column, key, value string) (string, []any, error) { + pattern, err := buildGlobPattern(key, value) + if err != nil { + return "", nil, err + } + return column + " NOT GLOB ?", []any{"*" + pattern + "*"}, nil +} + +func globKeyMissing(column, key string) (string, []any, error) { + pattern, err := buildGlobKeyPattern(key) + if err != nil { + return "", nil, err + } + return column + " NOT GLOB ?", []any{"*" + pattern + "*"}, nil +} + +// Search for `"key":"value"` +func buildGlobPattern(key, value string) (string, error) { + keyJSON, err := json.Marshal(key) + if err != nil { + return "", fmt.Errorf("failed to marshal key: %w", err) + } + valueJSON, err := json.Marshal(value) + if err != nil { + return "", fmt.Errorf("failed to marshal value: %w", err) + } + return escapeGlobPattern(fmt.Sprintf(`%s:%s`, string(keyJSON), string(valueJSON))), nil +} + +// Search for `"key":` +func buildGlobKeyPattern(key string) (string, error) { + keyJSON, err := json.Marshal(key) + if err != nil { + return "", fmt.Errorf("failed to marshal key: %w", err) + } + return escapeGlobPattern(string(keyJSON) + ":"), nil +} + +func escapeGlobPattern(pattern string) string { + pattern = strings.ReplaceAll(pattern, "[", "[[]") + pattern = strings.ReplaceAll(pattern, "*", "[*]") + pattern = strings.ReplaceAll(pattern, "?", "[?]") + return pattern +} diff --git a/pkg/services/ngalert/store/json_test.go b/pkg/services/ngalert/store/json_test.go new file mode 100644 index 00000000000..d09d3741c4b --- /dev/null +++ b/pkg/services/ngalert/store/json_test.go @@ -0,0 +1,185 @@ +package store + +import ( + "testing" + + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/stretchr/testify/require" +) + +func TestJsonEquals(t *testing.T) { + tests := []struct { + name string + dialect migrator.Dialect + column string + key string + value string + wantSQL string + wantArgs []any + }{ + { + name: "MySQL", + dialect: migrator.NewMysqlDialect(), + column: "labels", + key: "team", + value: "alerting", + wantSQL: "JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) = ?", + wantArgs: []any{"team", "alerting"}, + }, + { + name: "PostgreSQL", + dialect: migrator.NewPostgresDialect(), + column: "labels", + key: "team", + value: "alerting", + wantSQL: "jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) = ?", + wantArgs: []any{"team", "alerting"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, args := jsonEquals(tt.dialect, tt.column, tt.key, tt.value) + require.Equal(t, tt.wantSQL, sql) + require.Equal(t, tt.wantArgs, args) + }) + } +} + +func TestJsonNotEquals(t *testing.T) { + tests := []struct { + name string + dialect migrator.Dialect + column string + key string + value string + wantSQL string + wantArgs []any + }{ + { + name: "MySQL", + dialect: migrator.NewMysqlDialect(), + column: "labels", + key: "team", + value: "alerting", + wantSQL: "(JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) IS NULL OR JSON_UNQUOTE(JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?))) != ?)", + wantArgs: []any{"team", "team", "alerting"}, + }, + { + name: "PostgreSQL", + dialect: migrator.NewPostgresDialect(), + column: "labels", + key: "team", + value: "alerting", + wantSQL: "(jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) IS NULL OR jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) != ?)", + wantArgs: []any{"team", "team", "alerting"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, args := jsonNotEquals(tt.dialect, tt.column, tt.key, tt.value) + require.Equal(t, tt.wantSQL, sql) + require.Equal(t, tt.wantArgs, args) + }) + } +} + +func TestJsonKeyMissing(t *testing.T) { + tests := []struct { + name string + dialect migrator.Dialect + column string + key string + wantSQL string + wantArgs []any + }{ + { + name: "MySQL", + dialect: migrator.NewMysqlDialect(), + column: "labels", + key: "team", + wantSQL: "JSON_EXTRACT(NULLIF(labels, ''), CONCAT('$.', ?)) IS NULL", + wantArgs: []any{"team"}, + }, + { + name: "PostgreSQL", + dialect: migrator.NewPostgresDialect(), + column: "labels", + key: "team", + wantSQL: "jsonb_extract_path_text(NULLIF(labels, '')::jsonb, ?) IS NULL", + wantArgs: []any{"team"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, args := jsonKeyMissing(tt.dialect, tt.column, tt.key) + require.Equal(t, tt.wantSQL, sql) + require.Equal(t, tt.wantArgs, args) + }) + } +} + +func TestGlobEquals(t *testing.T) { + sql, args, err := globEquals("labels", "team", "alerting") + require.NoError(t, err) + require.Equal(t, "labels GLOB ?", sql) + require.Equal(t, []any{`*"team":"alerting"*`}, args) +} + +func TestGlobNotEquals(t *testing.T) { + sql, args, err := globNotEquals("labels", "team", "alerting") + require.NoError(t, err) + require.Equal(t, "labels NOT GLOB ?", sql) + require.Equal(t, []any{`*"team":"alerting"*`}, args) +} + +func TestGlobKeyMissing(t *testing.T) { + sql, args, err := globKeyMissing("labels", "team") + require.NoError(t, err) + require.Equal(t, "labels NOT GLOB ?", sql) + require.Equal(t, []any{`*"team":*`}, args) +} + +func TestBuildGlobPattern(t *testing.T) { + tests := []struct { + name string + key string + value string + expected string + }{ + { + name: "simple key-value", + key: "team", + value: "alerting", + expected: `"team":"alerting"`, + }, + { + name: "empty value", + key: "empty", + value: "", + expected: `"empty":""`, + }, + { + name: "special GLOB chars are escaped", + key: "key", + value: "*[?]", + expected: `"key":"[*][[][?]]"`, + }, + { + name: "special chars are escaped", + key: "key", + value: "line1\nline2\\end\"quote", + expected: `"key":"line1\nline2\\end\"quote"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pattern, err := buildGlobPattern(tt.key, tt.value) + require.NoError(t, err) + require.Equal(t, tt.expected, pattern) + }) + } +} diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go index 4c32cc2f879..a494c767a45 100644 --- a/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go @@ -35,6 +35,7 @@ type AlertInstance struct { ResolvedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` ResultFingerprint string `protobuf:"bytes,10,opt,name=result_fingerprint,json=resultFingerprint,proto3" json:"result_fingerprint,omitempty"` FiredAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=fired_at,json=firedAt,proto3" json:"fired_at,omitempty"` + Annotations map[string]string `protobuf:"bytes,12,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -146,6 +147,13 @@ func (x *AlertInstance) GetFiredAt() *timestamppb.Timestamp { return nil } +func (x *AlertInstance) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + type AlertInstances struct { state protoimpl.MessageState `protogen:"open.v1"` Instances []*AlertInstance `protobuf:"bytes,1,rep,name=instances,proto3" json:"instances,omitempty"` @@ -197,7 +205,7 @@ var file_alert_rule_state_proto_rawDesc = string([]byte{ 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb3, 0x05, 0x0a, 0x0d, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc7, 0x06, 0x0a, 0x0d, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x43, @@ -237,20 +245,29 @@ var file_alert_rule_state_proto_rawDesc = string([]byte{ 0x35, 0x0a, 0x08, 0x66, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 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, 0x07, 0x66, - 0x69, 0x72, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 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, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x4f, 0x0a, 0x0e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, - 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x73, 0x42, 0x40, 0x5a, 0x3e, 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, 0x6e, 0x67, - 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x72, 0x65, 0x64, 0x41, 0x74, 0x12, 0x52, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6e, 0x67, + 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x61, + 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 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, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3e, 0x0a, 0x10, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 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, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x0e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6e, 0x67, 0x61, + 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6c, + 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x09, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x42, 0x40, 0x5a, 0x3e, 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, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -265,27 +282,29 @@ func file_alert_rule_state_proto_rawDescGZIP() []byte { return file_alert_rule_state_proto_rawDescData } -var file_alert_rule_state_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_alert_rule_state_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_alert_rule_state_proto_goTypes = []any{ (*AlertInstance)(nil), // 0: ngalert.store.v1.AlertInstance (*AlertInstances)(nil), // 1: ngalert.store.v1.AlertInstances nil, // 2: ngalert.store.v1.AlertInstance.LabelsEntry - (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp + nil, // 3: ngalert.store.v1.AlertInstance.AnnotationsEntry + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp } var file_alert_rule_state_proto_depIdxs = []int32{ 2, // 0: ngalert.store.v1.AlertInstance.labels:type_name -> ngalert.store.v1.AlertInstance.LabelsEntry - 3, // 1: ngalert.store.v1.AlertInstance.current_state_since:type_name -> google.protobuf.Timestamp - 3, // 2: ngalert.store.v1.AlertInstance.current_state_end:type_name -> google.protobuf.Timestamp - 3, // 3: ngalert.store.v1.AlertInstance.last_eval_time:type_name -> google.protobuf.Timestamp - 3, // 4: ngalert.store.v1.AlertInstance.last_sent_at:type_name -> google.protobuf.Timestamp - 3, // 5: ngalert.store.v1.AlertInstance.resolved_at:type_name -> google.protobuf.Timestamp - 3, // 6: ngalert.store.v1.AlertInstance.fired_at:type_name -> google.protobuf.Timestamp - 0, // 7: ngalert.store.v1.AlertInstances.instances:type_name -> ngalert.store.v1.AlertInstance - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 4, // 1: ngalert.store.v1.AlertInstance.current_state_since:type_name -> google.protobuf.Timestamp + 4, // 2: ngalert.store.v1.AlertInstance.current_state_end:type_name -> google.protobuf.Timestamp + 4, // 3: ngalert.store.v1.AlertInstance.last_eval_time:type_name -> google.protobuf.Timestamp + 4, // 4: ngalert.store.v1.AlertInstance.last_sent_at:type_name -> google.protobuf.Timestamp + 4, // 5: ngalert.store.v1.AlertInstance.resolved_at:type_name -> google.protobuf.Timestamp + 4, // 6: ngalert.store.v1.AlertInstance.fired_at:type_name -> google.protobuf.Timestamp + 3, // 7: ngalert.store.v1.AlertInstance.annotations:type_name -> ngalert.store.v1.AlertInstance.AnnotationsEntry + 0, // 8: ngalert.store.v1.AlertInstances.instances:type_name -> ngalert.store.v1.AlertInstance + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_alert_rule_state_proto_init() } @@ -299,7 +318,7 @@ func file_alert_rule_state_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_alert_rule_state_proto_rawDesc), len(file_alert_rule_state_proto_rawDesc)), NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto index e80ac93cd09..5b48b422c27 100644 --- a/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto @@ -18,6 +18,7 @@ message AlertInstance { google.protobuf.Timestamp resolved_at = 9; string result_fingerprint = 10; google.protobuf.Timestamp fired_at = 11; + map annotations = 12; } message AlertInstances { diff --git a/pkg/services/ngalert/store/proto_instance_database.go b/pkg/services/ngalert/store/proto_instance_database.go index a9450d0851f..04ed45994e0 100644 --- a/pkg/services/ngalert/store/proto_instance_database.go +++ b/pkg/services/ngalert/store/proto_instance_database.go @@ -188,6 +188,7 @@ func alertInstanceModelToProto(modelInstance models.AlertInstance) *pb.AlertInst return &pb.AlertInstance{ Labels: modelInstance.Labels, LabelsHash: modelInstance.LabelsHash, + Annotations: modelInstance.Annotations, CurrentState: string(modelInstance.CurrentState), CurrentStateSince: timestamppb.New(modelInstance.CurrentStateSince), CurrentStateEnd: timestamppb.New(modelInstance.CurrentStateEnd), @@ -255,6 +256,7 @@ func alertInstanceProtoToModel(ruleUID string, ruleOrgID int64, protoInstance *p LabelsHash: protoInstance.LabelsHash, }, Labels: protoInstance.Labels, + Annotations: protoInstance.Annotations, CurrentState: models.InstanceStateType(protoInstance.CurrentState), CurrentStateSince: protoInstance.CurrentStateSince.AsTime(), CurrentStateEnd: protoInstance.CurrentStateEnd.AsTime(), diff --git a/pkg/services/ngalert/store/proto_instance_database_test.go b/pkg/services/ngalert/store/proto_instance_database_test.go index c98923995dd..b8e809a88e8 100644 --- a/pkg/services/ngalert/store/proto_instance_database_test.go +++ b/pkg/services/ngalert/store/proto_instance_database_test.go @@ -19,6 +19,7 @@ func TestAlertInstanceModelToProto(t *testing.T) { lastSentAt := currentStateSince.Add(-2 * time.Minute) firedAt := currentStateSince.Add(-2 * time.Minute) resolvedAt := currentStateSince.Add(-3 * time.Minute) + annotations := map[string]string{"summary": "value", "team": "alerting"} tests := []struct { name string @@ -28,7 +29,8 @@ func TestAlertInstanceModelToProto(t *testing.T) { { name: "valid instance", input: models.AlertInstance{ - Labels: map[string]string{"key": "value"}, + Labels: map[string]string{"key": "value"}, + Annotations: annotations, AlertInstanceKey: models.AlertInstanceKey{ RuleUID: "rule-uid-1", RuleOrgID: 1, @@ -46,6 +48,7 @@ func TestAlertInstanceModelToProto(t *testing.T) { }, expected: &pb.AlertInstance{ Labels: map[string]string{"key": "value"}, + Annotations: annotations, LabelsHash: "hash123", CurrentState: "Alerting", CurrentStateSince: timestamppb.New(currentStateSince), @@ -75,6 +78,7 @@ func TestAlertInstanceProtoToModel(t *testing.T) { lastSentAt := currentStateSince.Add(-2 * time.Minute).UTC() firedAt := currentStateSince.Add(-2 * time.Minute).UTC() resolvedAt := currentStateSince.Add(-3 * time.Minute).UTC() + annotations := map[string]string{"summary": "value", "team": "alerting"} ruleUID := "rule-uid-1" orgID := int64(1) @@ -87,6 +91,7 @@ func TestAlertInstanceProtoToModel(t *testing.T) { name: "valid instance", input: &pb.AlertInstance{ Labels: map[string]string{"key": "value"}, + Annotations: annotations, LabelsHash: "hash123", CurrentState: "Alerting", CurrentStateSince: timestamppb.New(currentStateSince), @@ -98,7 +103,8 @@ func TestAlertInstanceProtoToModel(t *testing.T) { ResultFingerprint: "fingerprint", }, expected: &models.AlertInstance{ - Labels: map[string]string{"key": "value"}, + Labels: map[string]string{"key": "value"}, + Annotations: annotations, AlertInstanceKey: models.AlertInstanceKey{ RuleUID: ruleUID, RuleOrgID: orgID, @@ -132,7 +138,7 @@ func TestModelAlertInstanceMatchesProtobuf(t *testing.T) { // and update them accordingly. t.Run("when AlertInstance model changes", func(t *testing.T) { modelType := reflect.TypeOf(models.AlertInstance{}) - require.Equal(t, 11, modelType.NumField(), "AlertInstance model has changed, update the protobuf") + require.Equal(t, 12, modelType.NumField(), "AlertInstance model has changed, update the protobuf") }) } @@ -142,6 +148,7 @@ func TestCompressAndDecompressAlertInstances(t *testing.T) { alertInstances := []*pb.AlertInstance{ { Labels: map[string]string{"label-1": "value-1"}, + Annotations: map[string]string{"anno-1": "value-1"}, LabelsHash: "hash-1", CurrentState: "normal", CurrentStateSince: timestamppb.New(now), @@ -154,6 +161,7 @@ func TestCompressAndDecompressAlertInstances(t *testing.T) { }, { Labels: map[string]string{"label-2": "value-2"}, + Annotations: map[string]string{"anno-2": "value-2"}, LabelsHash: "hash-2", CurrentState: "firing", CurrentStateSince: timestamppb.New(now), @@ -185,6 +193,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { LabelsHash: "hash-1", }, Labels: map[string]string{"label-1": "value-1"}, + Annotations: map[string]string{"anno-1": "value-1"}, CurrentState: models.InstanceStateFiring, CurrentStateSince: now, CurrentStateEnd: now.Add(time.Hour), @@ -202,6 +211,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { LabelsHash: "hash-2", }, Labels: map[string]string{"label-2": "value-2"}, + Annotations: map[string]string{"anno-2": "value-2"}, CurrentState: models.InstanceStateNormal, CurrentStateSince: now, CurrentStateEnd: now.Add(time.Hour), @@ -227,6 +237,7 @@ func TestConvertAndCompressAlertInstances(t *testing.T) { 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].Annotations, modelInstance.Annotations) require.Equal(t, modelInstances[i].CurrentState, modelInstance.CurrentState) require.Equal(t, modelInstances[i].LabelsHash, modelInstance.LabelsHash) require.Equal(t, modelInstances[i].ResultFingerprint, modelInstance.ResultFingerprint) diff --git a/pkg/services/ngalert/store/provisioning_store.go b/pkg/services/ngalert/store/provisioning_store.go index 27f03143c98..df5d7dc80c6 100644 --- a/pkg/services/ngalert/store/provisioning_store.go +++ b/pkg/services/ngalert/store/provisioning_store.go @@ -62,6 +62,30 @@ func (st DBstore) GetProvenances(ctx context.Context, org int64, resourceType st return resultMap, err } +// GetProvenancesByUIDs gets the provenance status for specific UIDs. +func (st DBstore) GetProvenancesByUIDs(ctx context.Context, org int64, resourceType string, uids []string) (map[string]models.Provenance, error) { + if len(uids) == 0 { + return map[string]models.Provenance{}, nil + } + + result := make(map[string]models.Provenance, len(uids)) + err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + rawData, err := sess.Table(provenanceRecord{}). + Where("record_type = ? AND org_id = ?", resourceType, org). + In("record_key", uids). + Cols("record_key", "provenance"). + QueryString() + if err != nil { + return fmt.Errorf("failed to query for existing provenance status: %w", err) + } + for _, data := range rawData { + result[data["record_key"]] = models.Provenance(data["provenance"]) + } + return nil + }) + return result, err +} + // SetProvenance changes the provenance status for a provisionable object. func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error { recordType := o.ResourceType() diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go index b6f8b8fe5cd..359d594da0a 100644 --- a/pkg/services/ngalert/store/provisioning_store_test.go +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -133,6 +133,55 @@ func TestIntegrationProvisioningStore(t *testing.T) { require.Equal(t, models.ProvenanceAPI, p[rule2.UID]) }) + t.Run("Store should return provenances by UIDs", func(t *testing.T) { + const orgID = 124 + rule1 := models.AlertRule{UID: "uid-1", OrgID: orgID} + rule2 := models.AlertRule{UID: "uid-2", OrgID: orgID} + rule3 := models.AlertRule{UID: "uid-3", OrgID: orgID} + + err := store.SetProvenance(context.Background(), &rule1, orgID, models.ProvenanceFile) + require.NoError(t, err) + err = store.SetProvenance(context.Background(), &rule2, orgID, models.ProvenanceAPI) + require.NoError(t, err) + err = store.SetProvenance(context.Background(), &rule3, orgID, models.ProvenanceFile) + require.NoError(t, err) + + // Fetch only rule1 and rule2 + p, err := store.GetProvenancesByUIDs(context.Background(), orgID, rule1.ResourceType(), []string{rule1.UID, rule2.UID}) + require.NoError(t, err) + require.Len(t, p, 2) + require.Equal(t, models.ProvenanceFile, p[rule1.UID]) + require.Equal(t, models.ProvenanceAPI, p[rule2.UID]) + _, exists := p[rule3.UID] + require.False(t, exists) + }) + + t.Run("GetProvenancesByUIDs returns empty map for empty UIDs", func(t *testing.T) { + p, err := store.GetProvenancesByUIDs(context.Background(), 1, "alertRule", []string{}) + require.NoError(t, err) + require.Empty(t, p) + }) + + t.Run("GetProvenancesByUIDs respects org ID", func(t *testing.T) { + const orgID1 = 125 + const orgID2 = 126 + rule := models.AlertRule{UID: "cross-org-uid"} + + err := store.SetProvenance(context.Background(), &rule, orgID1, models.ProvenanceFile) + require.NoError(t, err) + + // Should not find in different org + p, err := store.GetProvenancesByUIDs(context.Background(), orgID2, rule.ResourceType(), []string{rule.UID}) + require.NoError(t, err) + require.Empty(t, p) + + // Should find in correct org + p, err = store.GetProvenancesByUIDs(context.Background(), orgID1, rule.ResourceType(), []string{rule.UID}) + require.NoError(t, err) + require.Len(t, p, 1) + require.Equal(t, models.ProvenanceFile, p[rule.UID]) + }) + t.Run("Store should delete provenance correctly", func(t *testing.T) { const orgID = 1234 ruleOrg := models.AlertRule{ diff --git a/pkg/services/ngalert/tests/fakes/provisioning.go b/pkg/services/ngalert/tests/fakes/provisioning.go index 43de0a6dc68..fce2586f120 100644 --- a/pkg/services/ngalert/tests/fakes/provisioning.go +++ b/pkg/services/ngalert/tests/fakes/provisioning.go @@ -8,12 +8,13 @@ import ( ) type FakeProvisioningStore struct { - Calls []Call - Records map[int64]map[string]models.Provenance - GetProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) - GetProvenancesFunc func(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) - SetProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error - DeleteProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64) error + Calls []Call + Records map[int64]map[string]models.Provenance + GetProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) + GetProvenancesFunc func(ctx context.Context, orgID int64, resourceType string) (map[string]models.Provenance, error) + GetProvenancesByUIDsFunc func(ctx context.Context, orgID int64, resourceType string, uids []string) (map[string]models.Provenance, error) + SetProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error + DeleteProvenanceFunc func(ctx context.Context, o models.Provisionable, org int64) error } func NewFakeProvisioningStore() *FakeProvisioningStore { @@ -51,6 +52,23 @@ func (f *FakeProvisioningStore) GetProvenances(ctx context.Context, orgID int64, return results, nil } +func (f *FakeProvisioningStore) GetProvenancesByUIDs(ctx context.Context, orgID int64, resourceType string, uids []string) (map[string]models.Provenance, error) { + f.Calls = append(f.Calls, Call{MethodName: "GetProvenancesByUIDs", Arguments: []any{ctx, orgID, resourceType, uids}}) + if f.GetProvenancesByUIDsFunc != nil { + return f.GetProvenancesByUIDsFunc(ctx, orgID, resourceType, uids) + } + results := make(map[string]models.Provenance) + if val, ok := f.Records[orgID]; ok { + for _, uid := range uids { + key := uid + resourceType + if prov, ok := val[key]; ok { + results[uid] = prov + } + } + } + return results, nil +} + func (f *FakeProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error { f.Calls = append(f.Calls, Call{MethodName: "SetProvenance", Arguments: []any{ctx, o, org, p}}) if f.SetProvenanceFunc != nil { diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index cfe790b9853..f4b5afb282d 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -2,6 +2,7 @@ package fakes import ( "context" + "maps" "math/rand" "slices" "strings" @@ -219,6 +220,7 @@ func (f *RuleStore) ListAlertRulesByGroup(_ context.Context, q *models.ListAlert RuleUIDs: q.RuleUIDs, ReceiverName: q.ReceiverName, HasPrometheusRuleDefinition: q.HasPrometheusRuleDefinition, + LabelMatchers: q.LabelMatchers, } ruleList, err := f.listAlertRules(query) @@ -355,6 +357,20 @@ func (f *RuleStore) listAlertRules(q *models.ListAlertRulesQuery) (models.RulesG if q.ReceiverName != "" && (len(r.NotificationSettings) < 1 || r.NotificationSettings[0].Receiver != q.ReceiverName) { continue } + + if len(q.LabelMatchers) > 0 { + matches := true + for _, m := range q.LabelMatchers { + if !m.Matches(r.Labels[m.Name]) { + matches = false + break + } + } + if !matches { + continue + } + } + copyR := models.CopyRule(r) ruleList = append(ruleList, copyR) } @@ -597,6 +613,30 @@ func (f *RuleStore) GetAlertRuleVersions(_ context.Context, orgID int64, guid st return f.History[guid], nil } +func (f *RuleStore) GetAlertRuleVersionFolders(_ context.Context, orgID int64, guid string) ([]string, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + + q := GenericRecordedQuery{ + Name: "GetAlertRuleVersionFolders", + Params: []any{orgID, guid}, + } + defer func() { + f.RecordedOps = append(f.RecordedOps, q) + }() + + if err := f.Hook(q); err != nil { + return nil, err + } + + folderSet := make(map[string]struct{}) + for _, rule := range f.History[guid] { + folderSet[rule.NamespaceUID] = struct{}{} + } + + return slices.Collect(maps.Keys(folderSet)), nil +} + func (f *RuleStore) ListDeletedRules(_ context.Context, orgID int64) ([]*models.AlertRule, error) { f.mtx.Lock() defer f.mtx.Unlock() diff --git a/pkg/services/ngalert/writer/datasourcewriter.go b/pkg/services/ngalert/writer/datasourcewriter.go index cc7036974e7..3cb082fb639 100644 --- a/pkg/services/ngalert/writer/datasourcewriter.go +++ b/pkg/services/ngalert/writer/datasourcewriter.go @@ -217,6 +217,8 @@ func (w *DatasourceWriter) makeWriter(ctx context.Context, orgID int64, dsUID st } for k, values := range dsHeaders { + // Clear out any custom writer headers before adding the data souce ones. + headers.Del(k) for _, v := range values { headers.Add(k, v) } diff --git a/pkg/services/ngalert/writer/datasourcewriter_test.go b/pkg/services/ngalert/writer/datasourcewriter_test.go index 349d8b9191a..04c86a954b3 100644 --- a/pkg/services/ngalert/writer/datasourcewriter_test.go +++ b/pkg/services/ngalert/writer/datasourcewriter_test.go @@ -141,7 +141,7 @@ func setupDataSources(t *testing.T) *testDataSources { res.DataSourceHeaders["prom-4"] = http.Header{ "X-Scope-OrgID": []string{"test-user"}, "X-Test-Header": []string{"test-value"}, - "X-Double-Header": []string{"one", "two", "three"}, + "X-Double-Header": []string{"one", "two"}, } return res @@ -231,7 +231,7 @@ func TestDatasourceWriter(t *testing.T) { cHeaders := map[string]string{ "X-Custom-Header": "test-value", "X-Another-Header": "another-value", - overwrittenHeader: "overwritten", // Data source headers should be overwritten by custom headers. + overwrittenHeader: "should-be-overwritten", // Data source headers overwrite custom headers. } cfg = DatasourceWriterConfig{ @@ -249,17 +249,20 @@ func TestDatasourceWriter(t *testing.T) { require.Len(t, dsHeaders, 3) // We're confirming we have a data source header with the same name but different value. - // This one should not be sent in the request. require.NotEmpty(t, dsHeaders[overwrittenHeader]) require.NotEqual(t, dsHeaders[overwrittenHeader], cHeaders[overwrittenHeader]) - // All headers (except for the one that was overwritten) should have been used. + // All data source headers should have been used. for k, vv := range dsHeaders { - if k != overwrittenHeader { - assert.Equal(t, vv, testDS.prom4.LastHeaders.Values(k)) - } + assert.Equal(t, vv, testDS.prom4.LastHeaders.Values(k)) } + + // All custom headers except for the overwritten one should have been used. for k, v := range cHeaders { + if k == overwrittenHeader { + assert.NotEqual(t, v, testDS.prom4.LastHeaders.Get(k)) + continue + } assert.Equal(t, v, testDS.prom4.LastHeaders.Get(k)) } }) diff --git a/pkg/services/oauthtoken/oauth_token.go b/pkg/services/oauthtoken/oauth_token.go index 0efe5e553f3..6d320251ccc 100644 --- a/pkg/services/oauthtoken/oauth_token.go +++ b/pkg/services/oauthtoken/oauth_token.go @@ -660,6 +660,10 @@ func (o *Service) getExternalSession(ctx context.Context, usr identity.Requester return externalSessions[0], nil } + if sessionToken == nil { + return nil, auth.ErrExternalSessionTokenNotFound + } + // For regular users, we use the session token ID to fetch the external session return o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId) } diff --git a/pkg/services/org/model.go b/pkg/services/org/model.go index 7873e899eb3..ac0268e051c 100644 --- a/pkg/services/org/model.go +++ b/pkg/services/org/model.go @@ -188,6 +188,8 @@ type SearchOrgUsersQuery struct { SortOpts []model.SortOption // Flag used to allow oss edition to query users without access control DontEnforceAccessControl bool + // Flag used to exclude hidden users from the result + ExcludeHiddenUsers bool User identity.Requester } diff --git a/pkg/services/org/orgimpl/org.go b/pkg/services/org/orgimpl/org.go index 423a4bc8b8d..6df28368f4c 100644 --- a/pkg/services/org/orgimpl/org.go +++ b/pkg/services/org/orgimpl/org.go @@ -27,6 +27,7 @@ func ProvideService(db db.DB, cfg *setting.Cfg, quotaService quota.Service) (org db: db, dialect: db.GetDialect(), log: log, + cfg: cfg, }, cfg: cfg, log: log, diff --git a/pkg/services/org/orgimpl/store.go b/pkg/services/org/orgimpl/store.go index 50bbd68ec82..7e03db60e34 100644 --- a/pkg/services/org/orgimpl/store.go +++ b/pkg/services/org/orgimpl/store.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -16,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -53,6 +55,7 @@ type sqlStore struct { //TODO: moved to service log log.Logger deletes []string + cfg *setting.Cfg } func (ss *sqlStore) Get(ctx context.Context, orgID int64) (*org.Org, error) { @@ -560,6 +563,14 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser whereParams = append(whereParams, acFilter.Args...) } + if query.ExcludeHiddenUsers { + cond, params := buildHiddenUsersFilter(query.User, ss.cfg.HiddenUsers) + if cond != "" { + whereConditions = append(whereConditions, cond) + whereParams = append(whereParams, params...) + } + } + if query.Query != "" { sql1, param1 := ss.dialect.LikeOperator("email", true, query.Query, true) sql2, param2 := ss.dialect.LikeOperator("name", true, query.Query, true) @@ -825,3 +836,23 @@ func removeUserOrg(sess *db.Session, userID int64) error { func (ss *sqlStore) RegisterDelete(query string) { ss.deletes = append(ss.deletes, query) } + +func buildHiddenUsersFilter(requester identity.Requester, hiddenUsersMap map[string]struct{}) (string, []any) { + if requester != nil && requester.GetIsGrafanaAdmin() { + return "", nil + } + + hiddenUsers := make([]any, 0) + for user := range hiddenUsersMap { + if requester != nil && user == requester.GetLogin() { + continue + } + hiddenUsers = append(hiddenUsers, user) + } + + if len(hiddenUsers) > 0 { + return "u.login NOT IN (?" + strings.Repeat(",?", len(hiddenUsers)-1) + ")", hiddenUsers + } + + return "", nil +} diff --git a/pkg/services/org/orgimpl/store_test.go b/pkg/services/org/orgimpl/store_test.go index 5cd7c356a5c..54f8e9fda39 100644 --- a/pkg/services/org/orgimpl/store_test.go +++ b/pkg/services/org/orgimpl/store_test.go @@ -820,8 +820,9 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { db: store, dialect: store.GetDialect(), log: log.NewNopLogger(), + cfg: cfg, } - // orgUserStore.cfg.Skip + orgSvc, userSvc := createOrgAndUserSvc(t, store, cfg) o, err := orgSvc.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) @@ -829,6 +830,14 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { seedOrgUsers(t, &orgUserStore, 10, userSvc, o.ID) + user1, err := userSvc.GetByLogin(context.Background(), &user.GetUserByLoginQuery{LoginOrEmail: "user-1"}) + require.NoError(t, err) + + cfg.HiddenUsers = map[string]struct{}{ + "user-1": {}, + "user-2": {}, + } + tests := []struct { desc string query *org.SearchOrgUsersQuery @@ -840,7 +849,7 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { OrgID: o.ID, User: &user.SignedInUser{ OrgID: o.ID, - Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, }, }, expectedNumUsers: 10, @@ -851,7 +860,7 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { OrgID: o.ID, User: &user.SignedInUser{ OrgID: o.ID, - Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgUsersRead: {""}}}, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {""}}}, }, }, expectedNumUsers: 0, @@ -862,8 +871,8 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { OrgID: o.ID, User: &user.SignedInUser{ OrgID: o.ID, - Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgUsersRead: { - "users:id:1", + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: { + "users:id:2", "users:id:5", "users:id:9", }}}, @@ -871,6 +880,55 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { }, expectedNumUsers: 3, }, + { + desc: "should exclude hidden users when ExcludeHiddenUsers is true and user is nil", + query: &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: true, + User: nil, + DontEnforceAccessControl: true, + }, + expectedNumUsers: 8, + }, + { + desc: "should not exclude hidden users when ExcludeHiddenUsers is true and user is Grafana Admin", + query: &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: true, + User: &user.SignedInUser{ + OrgID: o.ID, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + }, + expectedNumUsers: 10, + }, + { + desc: "should return all users if ExcludeHiddenUsers is false", + query: &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: false, + User: &user.SignedInUser{ + OrgID: o.ID, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + }, + expectedNumUsers: 10, + }, + { + desc: "should include the hidden user when the request is made by the hidden user and ExcludeHiddenUsers is true", + query: &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: true, + User: &user.SignedInUser{ + UserID: user1.ID, + Login: user1.Login, + OrgID: o.ID, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + }, + expectedNumUsers: 9, + }, } for _, tt := range tests { @@ -879,13 +937,58 @@ func TestIntegration_SQLStore_SearchOrgUsers(t *testing.T) { require.NoError(t, err) assert.Len(t, result.OrgUsers, tt.expectedNumUsers) - if !hasWildcardScope(tt.query.User, accesscontrol.ActionOrgUsersRead) { + // No pagination is applied, so TotalCount should equal to number of returned users + assert.Equal(t, int64(tt.expectedNumUsers), result.TotalCount) + + if tt.query.User != nil && !hasWildcardScope(tt.query.User, accesscontrol.ActionOrgUsersRead) && !tt.query.User.GetIsGrafanaAdmin() { for _, u := range result.OrgUsers { assert.Contains(t, tt.query.User.GetPermissions()[accesscontrol.ActionOrgUsersRead], fmt.Sprintf("users:id:%d", u.UserID)) } } }) } + + t.Run("should paginate correctly when ExcludeHiddenUsers is true", func(t *testing.T) { + query := &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: true, + User: &user.SignedInUser{ + OrgID: o.ID, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + Limit: 5, + Page: 1, + } + result, err := orgUserStore.SearchOrgUsers(context.Background(), query) + require.NoError(t, err) + assert.Len(t, result.OrgUsers, 5) + assert.Equal(t, int64(8), result.TotalCount) + + query.Page = 2 + result, err = orgUserStore.SearchOrgUsers(context.Background(), query) + require.NoError(t, err) + assert.Len(t, result.OrgUsers, 3) + assert.Equal(t, int64(8), result.TotalCount) + }) + + t.Run("should return all users if HiddenUsers is empty", func(t *testing.T) { + oldHiddenUsers := cfg.HiddenUsers + cfg.HiddenUsers = make(map[string]struct{}) + defer func() { cfg.HiddenUsers = oldHiddenUsers }() + + query := &org.SearchOrgUsersQuery{ + OrgID: o.ID, + ExcludeHiddenUsers: true, + User: &user.SignedInUser{ + OrgID: o.ID, + Permissions: map[int64]map[string][]string{o.ID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + } + result, err := orgUserStore.SearchOrgUsers(context.Background(), query) + require.NoError(t, err) + assert.Len(t, result.OrgUsers, 10) + assert.Equal(t, int64(10), result.TotalCount) + }) } func TestIntegration_SQLStore_RemoveOrgUser(t *testing.T) { diff --git a/pkg/services/pluginsintegration/installsync/syncer.go b/pkg/services/pluginsintegration/installsync/syncer.go index 6352f7900fa..7c483d21d01 100644 --- a/pkg/services/pluginsintegration/installsync/syncer.go +++ b/pkg/services/pluginsintegration/installsync/syncer.go @@ -217,7 +217,6 @@ func (s *syncer) syncNamespace(ctx context.Context, namespace string, source ins err := s.installRegistrar.Register(ctx, namespace, &install.PluginInstall{ ID: p.ID, Version: p.Info.Version, - Class: install.Class(p.Class), Source: source, }) if err != nil { diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index 8dccc2c3522..f4504d254c5 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -17,6 +17,7 @@ type Plugin struct { // App fields Parent *ParentPlugin + Children []string IncludedInAppID string DefaultNavURL string Pinned bool @@ -85,6 +86,18 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { dto.Parent = &ParentPlugin{ID: p.Parent.ID} } + if len(p.Children) > 0 { + children := make([]string, 0, len(p.Children)) + for _, child := range p.Children { + if child != nil { + children = append(children, child.ID) + } + } + if len(children) > 0 { + dto.Children = children + } + } + return dto } diff --git a/pkg/services/provisioning/alerting/config_reader_test.go b/pkg/services/provisioning/alerting/config_reader_test.go index c13780633c3..9cb71b4950f 100644 --- a/pkg/services/provisioning/alerting/config_reader_test.go +++ b/pkg/services/provisioning/alerting/config_reader_test.go @@ -19,6 +19,7 @@ const ( testFileDasboardTypoSupport = "./testdata/alert_rules/dasboard-typo-support" testFileMultipleRules = "./testdata/alert_rules/multiple-rules" testFileMultipleFiles = "./testdata/alert_rules/multiple-files" + testFileRecordingRules = "./testdata/alert_rules/recording-rules" testFileCorrectProperties_cp = "./testdata/contact_points/correct-properties" testFileCorrectPropertiesWithOrg_cp = "./testdata/contact_points/correct-properties-with-org" testFileEmptyUID = "./testdata/contact_points/empty-uid" @@ -188,4 +189,43 @@ func TestConfigReader(t *testing.T) { } }) }) + + t.Run("recording rules should parse correctly", func(t *testing.T) { + ruleFiles, err := configReader.readConfig(ctx, testFileRecordingRules) + require.NoError(t, err) + require.Len(t, ruleFiles, 2) + + findRule := func(title string) *AlertingFile { + for _, rf := range ruleFiles { + if rf.Groups[0].Title == title { + return rf + } + } + return nil + } + + ruleWithTarget := findRule("recording_rules_group") + require.NotNil(t, ruleWithTarget) + + require.Len(t, ruleWithTarget.Groups, 1) + require.Len(t, ruleWithTarget.Groups[0].Rules, 1) + + ruleWith := ruleWithTarget.Groups[0].Rules[0] + require.NotNil(t, ruleWith.Record) + require.Equal(t, "my_recorded_metric", ruleWith.Record.Metric) + require.Equal(t, "A", ruleWith.Record.From) + require.Equal(t, "mimir-uid", ruleWith.Record.TargetDatasourceUID) + + ruleWithoutTarget := findRule("recording_rules_group_no_target") + require.NotNil(t, ruleWithoutTarget) + + require.Len(t, ruleWithoutTarget.Groups, 1) + require.Len(t, ruleWithoutTarget.Groups[0].Rules, 1) + + ruleWithout := ruleWithoutTarget.Groups[0].Rules[0] + require.NotNil(t, ruleWithout.Record) + require.Equal(t, "http_requests_rate", ruleWithout.Record.Metric) + require.Equal(t, "A", ruleWithout.Record.From) + require.Equal(t, "", ruleWithout.Record.TargetDatasourceUID) + }) } diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/rules_types.go index bac091ababa..46dbbbd6342 100644 --- a/pkg/services/provisioning/alerting/rules_types.go +++ b/pkg/services/provisioning/alerting/rules_types.go @@ -303,13 +303,15 @@ func (nsV1 *NotificationSettingsV1) mapToModel() (models.NotificationSettings, e } type RecordV1 struct { - Metric values.StringValue `json:"metric" yaml:"metric"` - From values.StringValue `json:"from" yaml:"from"` + Metric values.StringValue `json:"metric" yaml:"metric"` + From values.StringValue `json:"from" yaml:"from"` + TargetDatasourceUID values.StringValue `json:"targetDatasourceUid" yaml:"targetDatasourceUid"` } func (record *RecordV1) mapToModel() (models.Record, error) { return models.Record{ - Metric: record.Metric.Value(), - From: record.From.Value(), + Metric: record.Metric.Value(), + From: record.From.Value(), + TargetDatasourceUID: record.TargetDatasourceUID.Value(), }, nil } diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/rules_types_test.go index c2cff56e419..51db78097ad 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/rules_types_test.go @@ -208,6 +208,15 @@ func TestRecordingRules(t *testing.T) { _, err := rule.mapToModel(1) require.NoError(t, err) }) + + t.Run("a valid rule with empty targetDatasourceUid should not error", func(t *testing.T) { + rule := validRecordingRuleV1(t) + rule.Record.TargetDatasourceUID = stringToStringValue("") + model, err := rule.mapToModel(1) + require.NoError(t, err) + require.NotNil(t, model.Record) + require.Equal(t, "", model.Record.TargetDatasourceUID) + }) } func TestNotificationsSettingsV1MapToModel(t *testing.T) { @@ -307,80 +316,43 @@ func TestNotificationsSettingsV1MapToModel(t *testing.T) { func validRuleGroupV1(t *testing.T) AlertRuleGroupV1 { t.Helper() - var ( - orgID values.Int64Value - name values.StringValue - folder values.StringValue - interval values.StringValue - ) + + var orgID values.Int64Value err := yaml.Unmarshal([]byte("1"), &orgID) require.NoError(t, err) - err = yaml.Unmarshal([]byte("Test"), &name) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("Test"), &folder) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("10s"), &interval) - require.NoError(t, err) + return AlertRuleGroupV1{ OrgID: orgID, - Name: name, - Folder: folder, - Interval: interval, + Name: stringToStringValue("Test"), + Folder: stringToStringValue("Test"), + Interval: stringToStringValue("10s"), Rules: []AlertRuleV1{}, } } func validRuleV1(t *testing.T) AlertRuleV1 { t.Helper() - var ( - title values.StringValue - uid values.StringValue - forDuration values.StringValue - condition values.StringValue - ) - err := yaml.Unmarshal([]byte("test"), &title) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("test_uid"), &uid) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("10s"), &forDuration) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("A"), &condition) - require.NoError(t, err) + return AlertRuleV1{ - Title: title, - UID: uid, - For: forDuration, - Condition: condition, + Title: stringToStringValue("test"), + UID: stringToStringValue("test_uid"), + For: stringToStringValue("10s"), + Condition: stringToStringValue("A"), Data: []QueryV1{{}}, } } func validRecordingRuleV1(t *testing.T) AlertRuleV1 { t.Helper() - var ( - title values.StringValue - uid values.StringValue - forDuration values.StringValue - metric values.StringValue - from values.StringValue - ) - err := yaml.Unmarshal([]byte("test"), &title) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("test_uid"), &uid) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("10s"), &forDuration) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("test_metric"), &metric) - require.NoError(t, err) - err = yaml.Unmarshal([]byte("A"), &from) - require.NoError(t, err) + return AlertRuleV1{ - Title: title, - UID: uid, - For: forDuration, + Title: stringToStringValue("test"), + UID: stringToStringValue("test_uid"), + For: stringToStringValue("10s"), Record: &RecordV1{ - Metric: metric, - From: from, + Metric: stringToStringValue("test_metric"), + From: stringToStringValue("A"), + TargetDatasourceUID: stringToStringValue("test_target_datasource"), }, Data: []QueryV1{{}}, } diff --git a/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/with-target-datasource.yml b/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/with-target-datasource.yml new file mode 100644 index 00000000000..582ddf730ae --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/with-target-datasource.yml @@ -0,0 +1,26 @@ +apiVersion: 1 +groups: + - name: recording_rules_group + folder: my_folder + interval: 1m + rules: + - uid: recording_rule_with_target + title: my_recording_rule_with_target + condition: A + data: + - refId: A + queryType: '' + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus-uid + model: + expr: up{instance="localhost:9090"} + instant: true + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + record: + metric: my_recorded_metric + from: A + targetDatasourceUid: mimir-uid diff --git a/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/without-target-datasource.yml b/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/without-target-datasource.yml new file mode 100644 index 00000000000..0b8b2760988 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/alert_rules/recording-rules/without-target-datasource.yml @@ -0,0 +1,25 @@ +apiVersion: 1 +groups: + - name: recording_rules_group_no_target + folder: my_folder + interval: 1m + rules: + - uid: recording_rule_without_target + title: my_recording_rule_without_target + condition: A + data: + - refId: A + queryType: '' + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: prometheus-uid + model: + expr: rate(http_requests_total[5m]) + instant: true + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + record: + metric: http_requests_rate + from: A diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index 72b980e4198..36cacdaf12a 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -153,13 +153,20 @@ func (provider *Provisioner) Provision(ctx context.Context) error { // CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader. func (provider *Provisioner) CleanUpOrphanedDashboards(ctx context.Context) { - currentReaders := make([]string, len(provider.fileReaders)) + configs := make([]dashboards.ProvisioningConfig, len(provider.fileReaders)) for index, reader := range provider.fileReaders { - currentReaders[index] = reader.Cfg.Name + configs[index] = dashboards.ProvisioningConfig{ + Name: reader.Cfg.Name, + OrgID: reader.Cfg.OrgID, + Folder: reader.Cfg.Folder, + AllowUIUpdates: reader.Cfg.AllowUIUpdates, + } } - if err := provider.provisioner.DeleteOrphanedProvisionedDashboards(ctx, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: currentReaders}); err != nil { + if err := provider.provisioner.DeleteOrphanedProvisionedDashboards( + ctx, &dashboards.DeleteOrphanedProvisionedDashboardsCommand{Config: configs}, + ); err != nil { provider.log.Warn("Failed to delete orphaned provisioned dashboards", "err", err) } } diff --git a/pkg/services/setting/service.go b/pkg/services/setting/service.go index 5afc9e22159..0a249ef65ac 100644 --- a/pkg/services/setting/service.go +++ b/pkg/services/setting/service.go @@ -2,27 +2,27 @@ package setting import ( "context" + "encoding/json" "fmt" + "io" "net/http" "time" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "gopkg.in/ini.v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/client-go/dynamic" - clientrest "k8s.io/client-go/rest" + "k8s.io/client-go/rest" "k8s.io/client-go/transport" authlib "github.com/grafana/authlib/authn" logging "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/semconv" ) @@ -38,18 +38,11 @@ const ( ApiGroup = "setting.grafana.app" apiVersion = "v0alpha1" resource = "settings" - kind = "Setting" - listKind = "SettingList" ) -var settingGroupVersion = schema.GroupVersionResource{ - Group: ApiGroup, - Version: apiVersion, - Resource: resource, -} - -var settingGroupListKind = map[schema.GroupVersionResource]string{ - settingGroupVersion: listKind, +var settingGroupVersion = schema.GroupVersion{ + Group: ApiGroup, + Version: apiVersion, } type remoteSettingServiceMetrics struct { @@ -106,10 +99,10 @@ type Service interface { } type remoteSettingService struct { - dynamicClient dynamic.Interface - log logging.Logger - pageSize int64 - metrics remoteSettingServiceMetrics + restClient *rest.RESTClient + log logging.Logger + pageSize int64 + metrics remoteSettingServiceMetrics } var _ Service = (*remoteSettingService)(nil) @@ -126,7 +119,7 @@ type Config struct { // At least one of WrapTransport or TokenExchangeClient is required. WrapTransport transport.WrapperFunc // TLSClientConfig configures TLS for the client connection. - TLSClientConfig clientrest.TLSClientConfig + TLSClientConfig rest.TLSClientConfig // QPS limits requests per second (defaults to DefaultQPS). QPS float32 // Burst allows request bursts above QPS (defaults to DefaultBurst). @@ -145,29 +138,39 @@ type Setting struct { Value string `json:"value"` } +// settingResource represents a single Setting resource from the K8s API. +type settingResource struct { + Spec Setting `json:"spec"` +} + +// settingListMetadata contains pagination info from the K8s list response. +type settingListMetadata struct { + Continue string `json:"continue,omitempty"` +} + // New creates a Service from the provided configuration. func New(config Config) (Service, error) { log := logging.New(LogPrefix) - dynamicClient, err := getDynamicClient(config, log) + + restClient, err := getRestClient(config, log) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create REST client: %w", err) } + pageSize := DefaultPageSize if config.PageSize > 0 { pageSize = config.PageSize } - metrics := initMetrics() - return &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: pageSize, - log: log, - metrics: metrics, + restClient: restClient, + log: log, + pageSize: pageSize, + metrics: initMetrics(), }, nil } -func (m *remoteSettingService) ListAsIni(ctx context.Context, labelSelector metav1.LabelSelector) (*ini.File, error) { +func (s *remoteSettingService) ListAsIni(ctx context.Context, labelSelector metav1.LabelSelector) (*ini.File, error) { namespace, ok := request.NamespaceFrom(ctx) ns := semconv.GrafanaNamespaceName(namespace) ctx, span := tracer.Start(ctx, "remoteSettingService.ListAsIni", @@ -178,33 +181,34 @@ func (m *remoteSettingService) ListAsIni(ctx context.Context, labelSelector meta return nil, tracing.Errorf(span, "missing namespace in context") } - settings, err := m.List(ctx, labelSelector) + settings, err := s.List(ctx, labelSelector) if err != nil { return nil, err } - iniFile, err := m.toIni(settings) + iniFile, err := toIni(settings) if err != nil { return nil, tracing.Error(span, err) } return iniFile, nil } -func (m *remoteSettingService) List(ctx context.Context, labelSelector metav1.LabelSelector) ([]*Setting, error) { +func (s *remoteSettingService) List(ctx context.Context, labelSelector metav1.LabelSelector) ([]*Setting, error) { namespace, ok := request.NamespaceFrom(ctx) ns := semconv.GrafanaNamespaceName(namespace) ctx, span := tracer.Start(ctx, "remoteSettingService.List", trace.WithAttributes(ns)) defer span.End() + if !ok || namespace == "" { return nil, tracing.Errorf(span, "missing namespace in context") } - log := m.log.FromContext(ctx).New(ns.Key, ns.Value, "function", "remoteSettingService.List", "traceId", span.SpanContext().TraceID()) + log := s.log.FromContext(ctx).New(ns.Key, ns.Value, "function", "remoteSettingService.List", "traceId", span.SpanContext().TraceID()) startTime := time.Now() var status string defer func() { duration := time.Since(startTime).Seconds() - m.metrics.listDuration.WithLabelValues(status).Observe(duration) + s.metrics.listDuration.WithLabelValues(status).Observe(duration) }() selector, err := metav1.LabelSelectorAsSelector(&labelSelector) @@ -216,64 +220,142 @@ func (m *remoteSettingService) List(ctx context.Context, labelSelector metav1.La log.Debug("empty selector. Fetching all settings") } - var allSettings []*Setting + // Pre-allocate with estimated capacity + allSettings := make([]*Setting, 0, s.pageSize*8) var continueToken string hasNext := true totalPages := 0 // Using an upper limit to prevent infinite loops for hasNext && totalPages < 1000 { totalPages++ - opts := metav1.ListOptions{ - Limit: m.pageSize, - Continue: continueToken, - } - if !selector.Empty() { - opts.LabelSelector = selector.String() - } - settingsList, lErr := m.dynamicClient.Resource(settingGroupVersion).Namespace(namespace).List(ctx, opts) + settings, nextToken, lErr := s.fetchPage(ctx, namespace, selector.String(), continueToken) if lErr != nil { status = "error" return nil, tracing.Error(span, lErr) } - for i := range settingsList.Items { - setting, pErr := parseSettingResource(&settingsList.Items[i]) - if pErr != nil { - status = "error" - return nil, tracing.Error(span, pErr) - } - allSettings = append(allSettings, setting) - } - continueToken = settingsList.GetContinue() + + allSettings = append(allSettings, settings...) + continueToken = nextToken if continueToken == "" { hasNext = false } } status = "success" - m.metrics.listResultSize.WithLabelValues(status).Observe(float64(len(allSettings))) + s.metrics.listResultSize.WithLabelValues(status).Observe(float64(len(allSettings))) return allSettings, nil } -func parseSettingResource(setting *unstructured.Unstructured) (*Setting, error) { - spec, found, err := unstructured.NestedMap(setting.Object, "spec") +func (s *remoteSettingService) fetchPage(ctx context.Context, namespace, labelSelector, continueToken string) ([]*Setting, string, error) { + req := s.restClient.Get(). + Resource(resource). + Namespace(namespace). + Param("limit", fmt.Sprintf("%d", s.pageSize)) + + if labelSelector != "" { + req = req.Param("labelSelector", labelSelector) + } + if continueToken != "" { + req = req.Param("continue", continueToken) + } + + stream, err := req.Stream(ctx) if err != nil { - return nil, fmt.Errorf("failed to get spec from setting: %w", err) - } - if !found { - return nil, fmt.Errorf("spec not found in setting %s", setting.GetName()) + return nil, "", fmt.Errorf("request failed: %w", err) } + defer func() { _ = stream.Close() }() - var result Setting - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(spec, &result); err != nil { - return nil, fmt.Errorf("failed to convert spec to Setting: %w", err) - } - - return &result, nil + return parseSettingList(stream) } -func (m *remoteSettingService) toIni(settings []*Setting) (*ini.File, error) { +// parseSettingList parses a SettingList JSON response using token-by-token streaming. +func parseSettingList(r io.Reader) ([]*Setting, string, error) { + decoder := json.NewDecoder(r) + // Currently, first page may have a large number of items. + settings := make([]*Setting, 0, 1600) + var continueToken string + + // Skip to the start of the object + if _, err := decoder.Token(); err != nil { + return nil, "", fmt.Errorf("expected start of object: %w", err) + } + + for decoder.More() { + // Read field name + tok, err := decoder.Token() + if err != nil { + return nil, "", fmt.Errorf("failed to read field name: %w", err) + } + + fieldName, ok := tok.(string) + if !ok { + continue + } + + switch fieldName { + case "metadata": + var meta settingListMetadata + if err := decoder.Decode(&meta); err != nil { + return nil, "", fmt.Errorf("failed to decode metadata: %w", err) + } + continueToken = meta.Continue + + case "items": + // Parse items array token-by-token + itemSettings, err := parseItems(decoder) + if err != nil { + return nil, "", err + } + settings = append(settings, itemSettings...) + + default: + // Skip unknown fields + var skip json.RawMessage + if err := decoder.Decode(&skip); err != nil { + return nil, "", fmt.Errorf("failed to skip field %s: %w", fieldName, err) + } + } + } + + return settings, continueToken, nil +} + +func parseItems(decoder *json.Decoder) ([]*Setting, error) { + // Expect start of array + tok, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("expected start of items array: %w", err) + } + if tok != json.Delim('[') { + return nil, fmt.Errorf("expected '[', got %v", tok) + } + + settings := make([]*Setting, 0, DefaultPageSize) + + // Parse each item + for decoder.More() { + var item settingResource + if err := decoder.Decode(&item); err != nil { + return nil, fmt.Errorf("failed to decode setting item: %w", err) + } + settings = append(settings, &Setting{ + Section: item.Spec.Section, + Key: item.Spec.Key, + Value: item.Spec.Value, + }) + } + + // Consume end of array + if _, err := decoder.Token(); err != nil { + return nil, fmt.Errorf("expected end of items array: %w", err) + } + + return settings, nil +} + +func toIni(settings []*Setting) (*ini.File, error) { conf := ini.Empty() for _, setting := range settings { if !conf.HasSection(setting.Section) { @@ -287,7 +369,7 @@ func (m *remoteSettingService) toIni(settings []*Setting) (*ini.File, error) { return conf, nil } -func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, error) { +func getRestClient(config Config, log logging.Logger) (*rest.RESTClient, error) { if config.URL == "" { return nil, fmt.Errorf("URL cannot be empty") } @@ -296,7 +378,7 @@ func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, err } wrapTransport := config.WrapTransport - if config.WrapTransport == nil { + if wrapTransport == nil { log.Debug("using default wrapTransport with TokenExchangeClient") wrapTransport = func(rt http.RoundTripper) http.RoundTripper { return &authRoundTripper{ @@ -316,13 +398,24 @@ func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, err burst = config.Burst } - return dynamic.NewForConfig(&clientrest.Config{ + // Add a default scheme to handle K8s API error responses + scheme := runtime.NewScheme() + + restConfig := &rest.Config{ Host: config.URL, - WrapTransport: wrapTransport, TLSClientConfig: config.TLSClientConfig, + WrapTransport: wrapTransport, QPS: qps, Burst: burst, - }) + // Configure for our API group + APIPath: "/apis", + ContentConfig: rest.ContentConfig{ + GroupVersion: &settingGroupVersion, + NegotiatedSerializer: serializer.NewCodecFactory(scheme).WithoutConversion(), + }, + } + + return rest.RESTClientFor(restConfig) } // authRoundTripper wraps an HTTP transport with token-based authentication. @@ -341,10 +434,9 @@ func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) if err != nil { return nil, fmt.Errorf("failed to exchange token: %w", err) } - req = utilnet.CloneRequest(req) - - req.Header.Set("X-Access-Token", fmt.Sprintf("Bearer %s", token.Token)) - return a.transport.RoundTrip(req) + reqCopy := req.Clone(req.Context()) + reqCopy.Header.Set("X-Access-Token", fmt.Sprintf("Bearer %s", token.Token)) + return a.transport.RoundTrip(reqCopy) } func initMetrics() remoteSettingServiceMetrics { @@ -373,12 +465,12 @@ func initMetrics() remoteSettingServiceMetrics { return metrics } -func (m *remoteSettingService) Describe(descs chan<- *prometheus.Desc) { - m.metrics.listDuration.Describe(descs) - m.metrics.listResultSize.Describe(descs) +func (s *remoteSettingService) Describe(descs chan<- *prometheus.Desc) { + s.metrics.listDuration.Describe(descs) + s.metrics.listResultSize.Describe(descs) } -func (m *remoteSettingService) Collect(metrics chan<- prometheus.Metric) { - m.metrics.listDuration.Collect(metrics) - m.metrics.listResultSize.Collect(metrics) +func (s *remoteSettingService) Collect(metrics chan<- prometheus.Metric) { + s.metrics.listDuration.Collect(metrics) + s.metrics.listResultSize.Collect(metrics) } diff --git a/pkg/services/setting/service_test.go b/pkg/services/setting/service_test.go index 949ccc73a86..9b007fb1dc9 100644 --- a/pkg/services/setting/service_test.go +++ b/pkg/services/setting/service_test.go @@ -1,69 +1,36 @@ package setting import ( + "bytes" "context" "fmt" "net/http" + "net/http/httptest" + "strings" "testing" "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" "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/client-go/dynamic/fake" - k8testing "k8s.io/client-go/testing" - - authlib "github.com/grafana/authlib/authn" - "github.com/grafana/grafana/pkg/infra/log" ) func TestRemoteSettingService_ListAsIni(t *testing.T) { - t.Run("should filter settings by label selector", func(t *testing.T) { - // Create multiple settings, only some matching the selector - setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) - setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) - setting3 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "host", Value: "localhost"}) - - client := newTestClient(500, setting1, setting2, setting3) - - // Create a selector that should match only database settings - selector := metav1.LabelSelector{ - MatchLabels: map[string]string{ - "section": "database", - }, - } - - ctx := request.WithNamespace(context.Background(), "test-namespace") - result, err := client.ListAsIni(ctx, selector) - - require.NoError(t, err) - assert.NotNil(t, result) - // Should only have database settings, not server settings - assert.True(t, result.HasSection("database")) - assert.Equal(t, "postgres", result.Section("database").Key("type").String()) - assert.Equal(t, "localhost", result.Section("database").Key("host").String()) - // Should NOT have server settings - assert.False(t, result.HasSection("server")) - }) - t.Run("should return all settings with empty selector", func(t *testing.T) { - // Create multiple settings across different sections - setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) - setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "mysql"}) - - client := newTestClient(500, setting1, setting2) - - // Empty selector should select everything - selector := metav1.LabelSelector{} + settings := []Setting{ + {Section: "server", Key: "port", Value: "3000"}, + {Section: "database", Key: "type", Value: "mysql"}, + } + server := newTestServer(t, settings, "") + defer server.Close() + client := newTestClient(t, server.URL, 500) ctx := request.WithNamespace(context.Background(), "test-namespace") - result, err := client.ListAsIni(ctx, selector) + + result, err := client.ListAsIni(ctx, metav1.LabelSelector{}) require.NoError(t, err) assert.NotNil(t, result) - // Should have all settings from all sections assert.True(t, result.HasSection("server")) assert.Equal(t, "3000", result.Section("server").Key("port").String()) assert.True(t, result.HasSection("database")) @@ -73,209 +40,244 @@ func TestRemoteSettingService_ListAsIni(t *testing.T) { func TestRemoteSettingService_List(t *testing.T) { t.Run("should handle single page response", func(t *testing.T) { - setting := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) - - client := newTestClient(500, setting) + settings := []Setting{ + {Section: "server", Key: "port", Value: "3000"}, + } + server := newTestServer(t, settings, "") + defer server.Close() + client := newTestClient(t, server.URL, 500) ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) require.NoError(t, err) assert.Len(t, result, 1) - - spec := result[0] - assert.Equal(t, "server", spec.Section) - assert.Equal(t, "port", spec.Key) - assert.Equal(t, "3000", spec.Value) + assert.Equal(t, "server", result[0].Section) + assert.Equal(t, "port", result[0].Key) + assert.Equal(t, "3000", result[0].Value) }) - t.Run("should handle multiple pages", func(t *testing.T) { - totalPages := 3 - pageSize := 5 - - pages := make([][]*unstructured.Unstructured, totalPages) - for pageNum := 0; pageNum < totalPages; pageNum++ { - for idx := 0; idx < pageSize; idx++ { - item := newUnstructuredSetting( - "test-namespace", - Setting{ - Section: fmt.Sprintf("section-%d", pageNum), - Key: fmt.Sprintf("key-%d", idx), - Value: fmt.Sprintf("val-%d-%d", pageNum, idx), - }, - ) - pages[pageNum] = append(pages[pageNum], item) - } - } - - scheme := runtime.NewScheme() - dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) - listCallCount := 0 - dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { - listCallCount++ - - continueToken := fmt.Sprintf("continue-%d", listCallCount) - if listCallCount == totalPages { - continueToken = "" - } - - if listCallCount <= totalPages { - list := &unstructured.UnstructuredList{ - Object: map[string]interface{}{ - "apiVersion": ApiGroup + "/" + apiVersion, - "kind": listKind, - }, - } - list.SetContinue(continueToken) - for _, item := range pages[listCallCount-1] { - list.Items = append(list.Items, *item) - } - return true, list, nil - } - - return false, nil, nil - }) - - client := &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: int64(pageSize), - log: log.NewNopLogger(), - metrics: initMetrics(), + t.Run("should handle multiple settings", func(t *testing.T) { + settings := []Setting{ + {Section: "server", Key: "port", Value: "3000"}, + {Section: "database", Key: "host", Value: "localhost"}, + {Section: "database", Key: "port", Value: "5432"}, } + server := newTestServer(t, settings, "") + defer server.Close() + client := newTestClient(t, server.URL, 500) ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) require.NoError(t, err) - assert.Len(t, result, totalPages*pageSize) - assert.Equal(t, totalPages, listCallCount) + assert.Len(t, result, 3) }) - t.Run("should pass label selector when provided", func(t *testing.T) { - scheme := runtime.NewScheme() - dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) - dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { - listAction := action.(k8testing.ListActionImpl) - assert.Equal(t, "app=grafana", listAction.ListOptions.LabelSelector) - return true, &unstructured.UnstructuredList{}, nil - }) - - client := &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: 500, - log: log.NewNopLogger(), - metrics: initMetrics(), + t.Run("should handle pagination with continue token", func(t *testing.T) { + // First page + page1Settings := []Setting{ + {Section: "section-0", Key: "key-0", Value: "value-0"}, + {Section: "section-0", Key: "key-1", Value: "value-1"}, + } + // Second page + page2Settings := []Setting{ + {Section: "section-1", Key: "key-0", Value: "value-2"}, + {Section: "section-1", Key: "key-1", Value: "value-3"}, } + requestCount := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + continueToken := r.URL.Query().Get("continue") + + var settings []Setting + var nextContinue string + + if continueToken == "" { + settings = page1Settings + nextContinue = "page2" + } else { + settings = page2Settings + nextContinue = "" + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(generateSettingsJSON(settings, nextContinue))) + })) + defer server.Close() + + client := newTestClient(t, server.URL, 2) ctx := request.WithNamespace(context.Background(), "test-namespace") - _, err := client.List(ctx, metav1.LabelSelector{MatchLabels: map[string]string{"app": "grafana"}}) + + result, err := client.List(ctx, metav1.LabelSelector{}) require.NoError(t, err) + assert.Len(t, result, 4) + assert.Equal(t, 2, requestCount) }) - t.Run("should stop pagination at 1000 pages", func(t *testing.T) { - scheme := runtime.NewScheme() - dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) - listCallCount := 0 - dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { - listCallCount++ - // Always return a continue token to simulate infinite pagination - list := &unstructured.UnstructuredList{} - list.SetContinue("continue-forever") - return true, list, nil - }) + t.Run("should return error when namespace is missing", func(t *testing.T) { + server := newTestServer(t, nil, "") + defer server.Close() - client := &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: 10, - log: log.NewNopLogger(), - metrics: initMetrics(), - } + client := newTestClient(t, server.URL, 500) + ctx := context.Background() // No namespace - ctx := request.WithNamespace(context.Background(), "test-namespace") - _, err := client.List(ctx, metav1.LabelSelector{}) - - require.NoError(t, err) - assert.Equal(t, 1000, listCallCount, "Should stop at 1000 pages to prevent infinite loops") - }) - - t.Run("should return error when parsing setting fails", func(t *testing.T) { - scheme := runtime.NewScheme() - dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) - dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { - // Return a malformed setting without spec - list := &unstructured.UnstructuredList{ - Object: map[string]interface{}{ - "apiVersion": ApiGroup + "/" + apiVersion, - "kind": listKind, - }, - } - malformedSetting := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": ApiGroup + "/" + apiVersion, - "kind": kind, - "metadata": map[string]interface{}{ - "name": "malformed", - "namespace": "test-namespace", - }, - // Missing spec - }, - } - list.Items = append(list.Items, *malformedSetting) - return true, list, nil - }) - - client := &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: 500, - log: log.NewNopLogger(), - metrics: initMetrics(), - } - - ctx := request.WithNamespace(context.Background(), "test-namespace") result, err := client.List(ctx, metav1.LabelSelector{}) require.Error(t, err) assert.Nil(t, result) - assert.Contains(t, err.Error(), "spec not found") - }) -} - -func TestParseSettingResource(t *testing.T) { - t.Run("should parse valid setting resource", func(t *testing.T) { - setting := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) - - result, err := parseSettingResource(setting) - - require.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, "database", result.Section) - assert.Equal(t, "type", result.Key) - assert.Equal(t, "postgres", result.Value) + assert.Contains(t, err.Error(), "missing namespace") }) - t.Run("should return error when spec is missing", func(t *testing.T) { - setting := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": ApiGroup + "/" + apiVersion, - "kind": kind, - "metadata": map[string]interface{}{ - "name": "test-setting", - "namespace": "test-namespace", - }, - // No spec + t.Run("should return error on HTTP error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("internal server error")) + })) + defer server.Close() + + client := newTestClient(t, server.URL, 500) + ctx := request.WithNamespace(context.Background(), "test-namespace") + + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.Error(t, err) + assert.Nil(t, result) + }) + + t.Run("should handle API errors", func(t *testing.T) { + statusResponse := `{ + "apiVersion": "v1", + "kind": "Status", + "metadata": {}, + "status": "Failure", + "message": "settings.setting.grafana.app \"test\" not found", + "reason": "NotFound", + "details": { + "name": "test", + "group": "setting.grafana.app", + "kind": "settings" }, - } + "code": 404 + }` - result, err := parseSettingResource(setting) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(statusResponse)) + })) + defer server.Close() + + client := newTestClient(t, server.URL, 500) + ctx := request.WithNamespace(context.Background(), "test-namespace") + + result, err := client.List(ctx, metav1.LabelSelector{}) require.Error(t, err) assert.Nil(t, result) - assert.Contains(t, err.Error(), "spec not found") + assert.Contains(t, err.Error(), "could not find the requested resource") + }) + + t.Run("should handle 500 internal server error", func(t *testing.T) { + statusResponse := `{ + "apiVersion": "v1", + "kind": "Status", + "metadata": {}, + "status": "Failure", + "message": "Internal error occurred: database connection failed", + "reason": "InternalError", + "code": 500 + }` + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(statusResponse)) + })) + defer server.Close() + + client := newTestClient(t, server.URL, 500) + ctx := request.WithNamespace(context.Background(), "test-namespace") + + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "error on the server") + }) + + t.Run("should handle connection errors", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + serverURL := server.URL + server.Close() + + client := newTestClient(t, serverURL, 500) + ctx := request.WithNamespace(context.Background(), "test-namespace") + + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "connection refused") }) } -func TestRemoteSettingService_ToIni(t *testing.T) { +func TestParseSettingList(t *testing.T) { + t.Run("should parse valid settings list", func(t *testing.T) { + jsonData := `{ + "apiVersion": "setting.grafana.app/v0alpha1", + "kind": "SettingList", + "metadata": {"continue": ""}, + "items": [ + {"spec": {"section": "database", "key": "type", "value": "postgres"}}, + {"spec": {"section": "server", "key": "port", "value": "3000"}} + ] + }` + + settings, continueToken, err := parseSettingList(strings.NewReader(jsonData)) + + require.NoError(t, err) + assert.Len(t, settings, 2) + assert.Equal(t, "", continueToken) + assert.Equal(t, "database", settings[0].Section) + assert.Equal(t, "type", settings[0].Key) + assert.Equal(t, "postgres", settings[0].Value) + }) + + t.Run("should parse continue token", func(t *testing.T) { + jsonData := `{ + "apiVersion": "setting.grafana.app/v0alpha1", + "kind": "SettingList", + "metadata": {"continue": "next-page-token"}, + "items": [] + }` + + _, continueToken, err := parseSettingList(strings.NewReader(jsonData)) + + require.NoError(t, err) + assert.Equal(t, "next-page-token", continueToken) + }) + + t.Run("should handle empty items", func(t *testing.T) { + jsonData := `{ + "apiVersion": "setting.grafana.app/v0alpha1", + "kind": "SettingList", + "metadata": {}, + "items": [] + }` + + settings, _, err := parseSettingList(strings.NewReader(jsonData)) + + require.NoError(t, err) + assert.Len(t, settings, 0) + }) +} + +func TestToIni(t *testing.T) { t.Run("should convert settings to ini format", func(t *testing.T) { settings := []*Setting{ {Section: "database", Key: "type", Value: "postgres"}, @@ -283,12 +285,7 @@ func TestRemoteSettingService_ToIni(t *testing.T) { {Section: "server", Key: "http_port", Value: "3000"}, } - client := &remoteSettingService{ - pageSize: 500, - log: log.NewNopLogger(), - } - - result, err := client.toIni(settings) + result, err := toIni(settings) require.NoError(t, err) assert.NotNil(t, result) @@ -302,12 +299,7 @@ func TestRemoteSettingService_ToIni(t *testing.T) { t.Run("should handle empty settings list", func(t *testing.T) { var settings []*Setting - client := &remoteSettingService{ - pageSize: 500, - log: log.NewNopLogger(), - } - - result, err := client.toIni(settings) + result, err := toIni(settings) require.NoError(t, err) assert.NotNil(t, result) @@ -315,35 +307,13 @@ func TestRemoteSettingService_ToIni(t *testing.T) { assert.Len(t, sections, 1) // Only default section }) - t.Run("should create section if it does not exist", func(t *testing.T) { - settings := []*Setting{ - {Section: "new_section", Key: "new_key", Value: "new_value"}, - } - - client := &remoteSettingService{ - pageSize: 500, - log: log.NewNopLogger(), - } - - result, err := client.toIni(settings) - - require.NoError(t, err) - assert.True(t, result.HasSection("new_section")) - assert.Equal(t, "new_value", result.Section("new_section").Key("new_key").String()) - }) - t.Run("should handle multiple keys in same section", func(t *testing.T) { settings := []*Setting{ {Section: "auth", Key: "disable_login_form", Value: "false"}, {Section: "auth", Key: "disable_signout_menu", Value: "true"}, } - client := &remoteSettingService{ - pageSize: 500, - log: log.NewNopLogger(), - } - - result, err := client.toIni(settings) + result, err := toIni(settings) require.NoError(t, err) assert.True(t, result.HasSection("auth")) @@ -383,24 +353,23 @@ func TestNew(t *testing.T) { assert.Equal(t, int64(100), remoteClient.pageSize) }) - t.Run("should use default page size when zero is provided", func(t *testing.T) { + t.Run("should create client with custom QPS and Burst", func(t *testing.T) { config := Config{ URL: "https://example.com", WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, - PageSize: 0, + QPS: 50.0, + Burst: 100, } client, err := New(config) require.NoError(t, err) assert.NotNil(t, client) - remoteClient := client.(*remoteSettingService) - assert.Equal(t, DefaultPageSize, remoteClient.pageSize) }) - t.Run("should return error when config is invalid", func(t *testing.T) { + t.Run("should return error when URL is empty", func(t *testing.T) { config := Config{ - URL: "", // Invalid: empty URL + URL: "", } client, err := New(config) @@ -409,134 +378,126 @@ func TestNew(t *testing.T) { assert.Nil(t, client) assert.Contains(t, err.Error(), "URL cannot be empty") }) -} -func TestGetDynamicClient(t *testing.T) { - logger := log.NewNopLogger() - - t.Run("should return error when SettingServiceURL is empty", func(t *testing.T) { - config := Config{ - URL: "", - WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, - } - - client, err := getDynamicClient(config, logger) - - require.Error(t, err) - assert.Nil(t, client) - assert.Contains(t, err.Error(), "URL cannot be empty") - }) - - t.Run("should return error when both TokenExchangeClient and WrapTransport are nil", func(t *testing.T) { + t.Run("should return error when auth is not configured", func(t *testing.T) { config := Config{ URL: "https://example.com", TokenExchangeClient: nil, WrapTransport: nil, } - client, err := getDynamicClient(config, logger) + client, err := New(config) require.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport") }) - t.Run("should create client with WrapTransport", func(t *testing.T) { - config := Config{ - URL: "https://example.com", - WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, - } - - client, err := getDynamicClient(config, logger) - - require.NoError(t, err) - assert.NotNil(t, client) - }) - - t.Run("should not fail when QPS and Burst are not provided", func(t *testing.T) { - config := Config{ - URL: "https://example.com", - WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, - } - - client, err := getDynamicClient(config, logger) - - require.NoError(t, err) - assert.NotNil(t, client) - }) - - t.Run("should not fail when custom QPS and Burst are provided", func(t *testing.T) { - config := Config{ - URL: "https://example.com", - WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, - QPS: 10.0, - Burst: 20, - } - - client, err := getDynamicClient(config, logger) - - require.NoError(t, err) - assert.NotNil(t, client) - }) - - t.Run("should use WrapTransport when both WrapTransport and TokenExchangeClient are provided", func(t *testing.T) { + t.Run("should use WrapTransport when provided", func(t *testing.T) { wrapTransportCalled := false - tokenExchangeClient := &authlib.TokenExchangeClient{} config := Config{ - URL: "https://example.com", - TokenExchangeClient: tokenExchangeClient, + URL: "https://example.com", WrapTransport: func(rt http.RoundTripper) http.RoundTripper { wrapTransportCalled = true return rt }, } - client, err := getDynamicClient(config, logger) + client, err := New(config) require.NoError(t, err) assert.NotNil(t, client) - assert.True(t, wrapTransportCalled, "WrapTransport should be called and take precedence over TokenExchangeClient") + assert.True(t, wrapTransportCalled) }) } -// Helper function to create an unstructured Setting object for tests -func newUnstructuredSetting(namespace string, spec Setting) *unstructured.Unstructured { - // Generate resource name in the format {section}--{key} - name := fmt.Sprintf("%s--%s", spec.Section, spec.Key) +// Helper functions - obj := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": ApiGroup + "/" + apiVersion, - "kind": kind, - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, - }, - "spec": map[string]interface{}{ - "section": spec.Section, - "key": spec.Key, - "value": spec.Value, - }, - }, - } - // Always set section and key labels - obj.SetLabels(map[string]string{ - "section": spec.Section, - "key": spec.Key, - }) - return obj +func newTestServer(t *testing.T, settings []Setting, continueToken string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(generateSettingsJSON(settings, continueToken))) + })) } -// Helper function to create a test client with the dynamic fake client -func newTestClient(pageSize int64, objects ...runtime.Object) *remoteSettingService { - scheme := runtime.NewScheme() - dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind, objects...) +func newTestClient(t *testing.T, serverURL string, pageSize int64) Service { + t.Helper() + config := Config{ + URL: serverURL, + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + PageSize: pageSize, + } + client, err := New(config) + require.NoError(t, err) + return client +} - return &remoteSettingService{ - dynamicClient: dynamicClient, - pageSize: pageSize, - log: log.NewNopLogger(), - metrics: initMetrics(), +func generateSettingsJSON(settings []Setting, continueToken string) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf(`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"SettingList","metadata":{"continue":"%s"},"items":[`, continueToken)) + + for i, s := range settings { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(fmt.Sprintf( + `{"apiVersion":"setting.grafana.app/v0alpha1","kind":"Setting","metadata":{"name":"%s--%s","namespace":"test-namespace"},"spec":{"section":"%s","key":"%s","value":"%s"}}`, + s.Section, s.Key, s.Section, s.Key, s.Value, + )) + } + + sb.WriteString(`]}`) + return sb.String() +} + +// Benchmark tests for streaming JSON parser + +func BenchmarkParseSettingList(b *testing.B) { + jsonData := generateSettingListJSON(4000, 100) + jsonBytes := []byte(jsonData) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + reader := bytes.NewReader(jsonBytes) + _, _, _ = parseSettingList(reader) } } + +func BenchmarkParseSettingList_SinglePage(b *testing.B) { + jsonData := generateSettingListJSON(500, 50) + jsonBytes := []byte(jsonData) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + reader := bytes.NewReader(jsonBytes) + _, _, _ = parseSettingList(reader) + } +} + +// generateSettingListJSON generates a K8s-style SettingList JSON response for benchmarks +func generateSettingListJSON(totalSettings, numSections int) string { + var sb strings.Builder + sb.WriteString(`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"SettingList","metadata":{"continue":""},"items":[`) + + settingsPerSection := totalSettings / numSections + first := true + for section := 0; section < numSections; section++ { + for key := 0; key < settingsPerSection; key++ { + if !first { + sb.WriteString(",") + } + first = false + sb.WriteString(fmt.Sprintf( + `{"apiVersion":"setting.grafana.app/v0alpha1","kind":"Setting","metadata":{"name":"section-%03d--key-%03d","namespace":"bench-ns"},"spec":{"section":"section-%03d","key":"key-%03d","value":"value-for-section-%d-key-%d"}}`, + section, key, section, key, section, key, + )) + } + } + + sb.WriteString(`]}`) + return sb.String() +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/alerting.go b/pkg/services/sqlstore/migrations/accesscontrol/alerting.go index f9942fac80e..deddf59eb12 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/alerting.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/alerting.go @@ -116,3 +116,49 @@ func (m *receiverCreateScopeMigration) Exec(sess *xorm.Session, mg *migrator.Mig func AddReceiverCreateScopeMigration(mg *migrator.Migrator) { mg.AddMigration("remove scope from alert.notifications.receivers:create", &receiverCreateScopeMigration{}) } + +type receiverProtectedFieldsEditor struct { + migrator.MigrationBase +} + +var _ migrator.CodeMigration = new(alertingMigrator) + +func (m *receiverProtectedFieldsEditor) SQL(migrator.Dialect) string { + return "code migration" +} + +func (m *receiverProtectedFieldsEditor) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + sql := `SELECT * + FROM permission AS P + WHERE action = 'alert.notifications.receivers.secrets:read' + AND EXISTS(SELECT 1 FROM role AS R WHERE R.id = P.role_id AND R.name LIKE 'managed:%') + AND NOT EXISTS(SELECT 1 + FROM permission AS P2 + WHERE P2.role_id = P.role_id + AND P2.action = 'alert.notifications.receivers.protected:write' AND P2.scope = P.scope + )` + var results []accesscontrol.Permission + if err := sess.SQL(sql).Find(&results); err != nil { + return fmt.Errorf("failed to query permissions: %w", err) + } + + permissionsToCreate := make([]accesscontrol.Permission, 0, len(results)) + rolesAffected := make(map[int64][]string, 0) + for _, result := range results { + result.ID = 0 + result.Action = "alert.notifications.receivers.protected:write" + result.Created = time.Now() + result.Updated = time.Now() + permissionsToCreate = append(permissionsToCreate, result) + rolesAffected[result.RoleID] = append(rolesAffected[result.RoleID], result.Identifier) + } + _, err := sess.InsertMulti(&permissionsToCreate) + for id, ids := range rolesAffected { + mg.Logger.Debug("Added permission 'alert.notifications.receivers.protected:write' to managed role", "roleID", id, "identifiers", ids) + } + return err +} + +func AddReceiverProtectedFieldsEditor(mg *migrator.Migrator) { + mg.AddMigration("add 'alert.notifications.receivers.protected:write' to receiver admins", &receiverProtectedFieldsEditor{}) +} diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index 914f73819fc..d1cd33b108f 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -261,8 +261,8 @@ func RunDashboardUIDMigrations(sess *xorm.Session, driverName string, logger log logger.Info("Starting batched dashboard_uid migration for annotations (newest first)", "batchSize", batchSize) updateSQL := `UPDATE annotation SET dashboard_uid = (SELECT uid FROM dashboard WHERE dashboard.id = annotation.dashboard_id) - WHERE dashboard_uid IS NULL - AND dashboard_id != 0 + WHERE dashboard_uid IS NULL + AND dashboard_id != 0 AND EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = annotation.dashboard_id) AND annotation.id IN ( SELECT id FROM annotation @@ -285,19 +285,19 @@ func RunDashboardUIDMigrations(sess *xorm.Session, driverName string, logger log LIMIT $1 )` case MySQL: - updateSQL = `UPDATE annotation - INNER JOIN dashboard ON annotation.dashboard_id = dashboard.id - SET annotation.dashboard_uid = dashboard.uid - WHERE annotation.dashboard_uid IS NULL - AND annotation.dashboard_id != 0 - AND annotation.id IN ( - SELECT id FROM ( - SELECT id FROM annotation - WHERE dashboard_uid IS NULL AND dashboard_id != 0 - ORDER BY id DESC - LIMIT ? - ) AS batch - )` + updateSQL = `UPDATE annotation AS a + JOIN dashboard AS d ON a.dashboard_id = d.id + JOIN ( + SELECT id + FROM annotation + WHERE dashboard_uid IS NULL + AND dashboard_id != 0 + ORDER BY id DESC + LIMIT ? + ) AS batch ON batch.id = a.id + SET a.dashboard_uid = d.uid + WHERE a.dashboard_uid IS NULL + AND a.dashboard_id != 0` } updatedTotal := int64(0) diff --git a/pkg/services/sqlstore/migrations/cloud_migrations.go b/pkg/services/sqlstore/migrations/cloud_migrations.go index 7a602bf7d42..c6240037d04 100644 --- a/pkg/services/sqlstore/migrations/cloud_migrations.go +++ b/pkg/services/sqlstore/migrations/cloud_migrations.go @@ -198,10 +198,11 @@ func addCloudMigrationsMigrations(mg *Migrator) { Postgres("ALTER TABLE cloud_migration_resource ALTER COLUMN resource_uid TYPE VARCHAR(255);")) mg.AddMigration("create cloud_migration_snapshot_partition table v1", NewAddTableMigration(migrationSnapshotPartitionTable)) - mg.AddMigration("add cloud_migration_snapshot_partition srp_unique index", NewAddIndexMigration(migrationSnapshotPartitionTable, &Index{ + srpUniqueIndex := Index{ Name: "srp_unique", Cols: []string{"snapshot_uid", "resource_type", "partition_number"}, Type: UniqueIndex, - })) + } + mg.AddMigration("add cloud_migration_snapshot_partition srp_unique index", NewAddIndexMigration(migrationSnapshotPartitionTable, &srpUniqueIndex)) mg.AddMigration("add resource_storage_type column to cloud_migration_snapshot table", NewAddColumnMigration(migrationSnapshotTable, &Column{ Name: "resource_storage_type", Type: DB_Varchar, @@ -224,4 +225,16 @@ func addCloudMigrationsMigrations(mg *Migrator) { Type: DB_Blob, Nullable: true, })) + + updatedCloudMigrationSnapshotPartitionTable := Table{ + Name: "cloud_migration_snapshot_partition", + Columns: []*Column{ + {Name: "snapshot_uid", Type: DB_NVarchar, Length: 40, Nullable: false, IsPrimaryKey: true}, + {Name: "partition_number", Type: DB_Int, Nullable: false, IsPrimaryKey: true}, + {Name: "resource_type", Type: DB_Varchar, Length: 255, Nullable: false, IsPrimaryKey: true}, + {Name: "data", Type: DB_LongBlob, Nullable: false}, + }, + PrimaryKeys: []string{"snapshot_uid", "resource_type", "partition_number"}, + } + ConvertUniqueKeyToPrimaryKey(mg, srpUniqueIndex, updatedCloudMigrationSnapshotPartitionTable) } diff --git a/pkg/services/sqlstore/migrations/db_file_storage.go b/pkg/services/sqlstore/migrations/db_file_storage.go index e8ca9348455..c44344307ac 100644 --- a/pkg/services/sqlstore/migrations/db_file_storage.go +++ b/pkg/services/sqlstore/migrations/db_file_storage.go @@ -91,7 +91,7 @@ func convertFilePathHashIndexToPrimaryKey(mg *migrator.Migrator) { mg.AddMigration("drop file_path unique index from file table if it exists (mysql)", mysqlMigration2) mysqlMigration3 := migrator.NewRawSQLMigration("").Mysql(`ALTER TABLE file ADD PRIMARY KEY (path_hash);`) - mysqlMigration3.Condition = &migrator.IfPrimaryKeyNotExistsCondition{TableName: "file", ColumnName: "path_hash"} + mysqlMigration3.Condition = &migrator.IfPrimaryKeyNotExistsCondition{TableName: "file"} mg.AddMigration("add primary key to file table if it doesn't exist (mysql)", mysqlMigration3) postgres := ` @@ -162,7 +162,7 @@ func convertFileMetaPathHashKeyIndexToPrimaryKey(mg *migrator.Migrator) { mg.AddMigration("drop file_path unique index from file_meta table if it exists (mysql)", mysqlMigration2) mysqlMigration3 := migrator.NewRawSQLMigration("").Mysql(`ALTER TABLE file_meta ADD PRIMARY KEY (path_hash, ` + "`key`" + `);`) - mysqlMigration3.Condition = &migrator.IfPrimaryKeyNotExistsCondition{TableName: "file_meta", ColumnName: "path_hash"} + mysqlMigration3.Condition = &migrator.IfPrimaryKeyNotExistsCondition{TableName: "file_meta"} mg.AddMigration("add primary key to file_meta table if it doesn't exist (mysql)", mysqlMigration3) postgres := ` diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 4fc4e1df131..17dd7b9c068 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -163,5 +163,11 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.AddAlertRuleGroupIndexMigration(mg) + ualert.AddStateAnnotationsColumn(mg) + + ualert.CollateBinAlertRuleNamespace(mg) + ualert.CollateBinAlertRuleGroup(mg) + + accesscontrol.AddReceiverProtectedFieldsEditor(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_namespace_collation.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_namespace_collation.go new file mode 100644 index 00000000000..2766f337baa --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_namespace_collation.go @@ -0,0 +1,10 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// CollateBinAlertRuleNamespace ensures that namespace_uid column collates in the same way go sorts strings. +func CollateBinAlertRuleNamespace(mg *migrator.Migrator) { + mg.AddMigration("ensure namespace_uid column sorts the same way as golang", migrator.NewRawSQLMigration(""). + Mysql("ALTER TABLE alert_rule MODIFY namespace_uid VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;"). + Postgres(`ALTER TABLE alert_rule ALTER COLUMN namespace_uid SET DATA TYPE varchar(40) COLLATE "C";`)) +} diff --git a/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go b/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go new file mode 100644 index 00000000000..e4f599c9f44 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/state_annotations_mig.go @@ -0,0 +1,12 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// AddStateAnnotationsColumn adds annotations column to alert_instance +func AddStateAnnotationsColumn(mg *migrator.Migrator) { + mg.AddMigration("add annotations column to alert_instance table", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_instance"}, &migrator.Column{ + Name: "annotations", + Type: migrator.DB_Text, + Nullable: true, + })) +} diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index bec421e36e2..d75f87989f7 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/util/xorm" ) @@ -77,7 +78,7 @@ type Dialect interface { // CreateDatabaseFromSnapshot is called when migration log table is not found. // Dialect can recreate all tables from existing snapshot. After successful (nil error) return, // migrator will list migrations from the log, and apply all missing migrations. - CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, migrationLogTableName string) error + CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, migrationLogTableName string, logger log.Logger) error IsUniqueConstraintViolation(err error) bool ErrorMessage(err error) string @@ -253,7 +254,7 @@ func (b *BaseDialect) CopyTableData(sourceTable string, targetTable string, sour targetColsSQL := b.QuoteColList(targetCols) quote := b.dialect.Quote - return fmt.Sprintf("INSERT INTO %s (%s) SELECT %s FROM %s", quote(targetTable), targetColsSQL, sourceColsSQL, quote(sourceTable)) + return fmt.Sprintf("INSERT INTO %s (%s)\nSELECT %s\nFROM %s", quote(targetTable), targetColsSQL, sourceColsSQL, quote(sourceTable)) } func (b *BaseDialect) DropTable(tableName string) string { @@ -355,7 +356,7 @@ func (b *BaseDialect) CleanDB(engine *xorm.Engine) error { return nil } -func (b *BaseDialect) CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, tableName string) error { +func (b *BaseDialect) CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, migrationLogTableName string, logger log.Logger) error { return nil } diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go index b785fd867d5..ebc9573d111 100644 --- a/pkg/services/sqlstore/migrator/migrations.go +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -1,6 +1,8 @@ package migrator import ( + "fmt" + "slices" "strings" ) @@ -271,3 +273,155 @@ func NewTableCharsetMigration(tableName string, columns []*Column) *TableCharset func (m *TableCharsetMigration) SQL(d Dialect) string { return d.UpdateTableSQL(m.tableName, m.columns) } + +type addPrimaryKeyMigration struct { + MigrationBase + tableName string + uniqueKey Index + + // Used for Sqlite recreation of the table. Temporary table will have tableName + "_new" suffix. + table Table +} + +func (m *addPrimaryKeyMigration) SQL(d Dialect) string { + if d.DriverName() == SQLite { + // Final SQL will do following in the individual statements: + // 1. Create new temporary table + // 2. Copy data from old table to temporary table + // 3. Drop old table, rename temporary table to original name + // 4. Recreate indexes for table. + // + // For example: + // + // CREATE TABLE file_new + // ( + // path TEXT NOT NULL, + // path_hash TEXT NOT NULL, + // parent_folder_path_hash TEXT NOT NULL, + // contents BLOB NOT NULL, + // etag TEXT NOT NULL, + // cache_control TEXT NOT NULL, + // content_disposition TEXT NOT NULL, + // updated DATETIME NOT NULL, + // created DATETIME NOT NULL, + // size INTEGER NOT NULL, + // mime_type TEXT NOT NULL, + // + // PRIMARY KEY (path_hash) + // ); + // + // INSERT INTO file_new (path, path_hash, parent_folder_path_hash, contents, etag, cache_control, content_disposition, updated, created, size, mime_type) + // SELECT path, path_hash, parent_folder_path_hash, contents, etag, cache_control, content_disposition, updated, created, size, mime_type FROM file; + // + // DROP TABLE file; + // ALTER TABLE file_new RENAME TO file; + // + // CREATE INDEX IDX_file_parent_folder_path_hash ON file (parent_folder_path_hash); + + tempTable := m.table + tempTable.Name = m.tableName + "_new" + + statements := strings.Builder{} + + statements.WriteString(d.CreateTableSQL(&tempTable)) + statements.WriteString("\n") // CreateTableSQL adds semicolon + + cols := make([]string, 0, len(tempTable.Columns)) + for _, col := range tempTable.Columns { + cols = append(cols, col.Name) + } + statements.WriteString(d.CopyTableData(m.tableName, tempTable.Name, cols, cols)) + statements.WriteString(";\n") + + statements.WriteString(d.DropTable(m.tableName)) + statements.WriteString(";\n") + + statements.WriteString(d.RenameTable(tempTable.Name, m.tableName)) + statements.WriteString(";\n") + + for _, idx := range tempTable.Indices { + // Use real table name, not temporary one now + statements.WriteString(d.CreateIndexSQL(m.tableName, idx)) + statements.WriteString("\n") // CreateIndexSQL adds semicolon + } + + return statements.String() + } else if d.DriverName() == Postgres { + quotesCols := make([]string, 0, len(m.uniqueKey.Cols)) + for _, c := range m.uniqueKey.Cols { + quotesCols = append(quotesCols, d.Quote(c)) + } + + return fmt.Sprintf(` + DO $$ + BEGIN + -- Drop the unique constraint if it exists + DROP INDEX IF EXISTS %s; + + -- Add primary key if it doesn't already exist + IF NOT EXISTS (SELECT 1 FROM pg_index i WHERE indrelid = '%s'::regclass AND indisprimary) THEN + ALTER TABLE %s ADD PRIMARY KEY (%s); + END IF; + END $$;`, d.Quote(m.uniqueKey.XName(m.tableName)), m.tableName, d.Quote(m.tableName), strings.Join(quotesCols, ",")) + } else { + return "" + } +} + +// ConvertUniqueKeyToPrimaryKey adds series of migrations to convert existing unique key to PRIMARY KEY. +// For Sqlite this means recreating the table, which only works if there are no foreign keys referencing the table. +func ConvertUniqueKeyToPrimaryKey(mg *Migrator, uniqueKey Index, finalTable Table) { + tableName := finalTable.Name + if tableName == "" { + panic("invalid table name") + } + if len(uniqueKey.Cols) == 0 || uniqueKey.Type != UniqueIndex { + panic("invalid unique type") + } + if !slices.Equal(uniqueKey.Cols, finalTable.PrimaryKeys) { + panic("invalid primary key in the final table") + } + + colPks := map[string]bool{} + for _, col := range finalTable.Columns { + if col.IsPrimaryKey { + colPks[col.Name] = true + } + } + for _, c := range uniqueKey.Cols { + if !colPks[c] { + panic(fmt.Sprintf("column %s is not part of primary key in the table definition", c)) + } + } + + columnsList := strings.Join(uniqueKey.Cols, ",") + + mysqlQuote := NewDialect(MySQL).Quote + mysqlQuotedColumns := make([]string, 0, len(uniqueKey.Cols)) + for _, col := range uniqueKey.Cols { + mysqlQuotedColumns = append(mysqlQuotedColumns, mysqlQuote(col)) + } + + // migration 1 is to handle cases where the table was created with sql_generate_invisible_primary_key = ON + // in this case we need to do the conversion in one sql statement + mysqlMigration1 := NewRawSQLMigration("").Mysql(fmt.Sprintf(` + ALTER TABLE %s + DROP PRIMARY KEY, + DROP COLUMN my_row_id, + DROP INDEX %s, + ADD PRIMARY KEY (%s); + `, tableName, uniqueKey.XName(tableName), strings.Join(mysqlQuotedColumns, ","))) + mysqlMigration1.Condition = &IfColumnExistsCondition{TableName: tableName, ColumnName: "my_row_id"} + mg.AddMigration(fmt.Sprintf("drop my_row_id and add primary key with columns %s to table %s if my_row_id exists (auto-generated mysql column)", columnsList, tableName), mysqlMigration1) + + mysqlMigration2 := NewRawSQLMigration("").Mysql(fmt.Sprintf(`ALTER TABLE %s DROP INDEX %s`, tableName, uniqueKey.XName(tableName))) + mysqlMigration2.Condition = &IfIndexExistsCondition{TableName: tableName, IndexName: uniqueKey.XName(tableName)} + mg.AddMigration(fmt.Sprintf("drop unique index %s from %s table if it exists (mysql)", uniqueKey.XName(tableName), tableName), mysqlMigration2) + + mysqlMigration3 := NewRawSQLMigration("").Mysql(fmt.Sprintf(`ALTER TABLE %s ADD PRIMARY KEY (%s)`, tableName, strings.Join(mysqlQuotedColumns, ","))) + mysqlMigration3.Condition = &IfPrimaryKeyNotExistsCondition{TableName: tableName} + mg.AddMigration(fmt.Sprintf("add primary key with columns %s to table %s if it doesn't exist (mysql)", columnsList, tableName), mysqlMigration3) + + // postgres and sqlite statements are idempotent so we can have only one condition-less migration + mg.AddMigration(fmt.Sprintf("add primary key with columns %s to table %s (postgres and sqlite)", columnsList, tableName), &addPrimaryKeyMigration{tableName: tableName, uniqueKey: uniqueKey, table: finalTable}) +} diff --git a/pkg/services/sqlstore/migrator/migrations_test.go b/pkg/services/sqlstore/migrator/migrations_test.go new file mode 100644 index 00000000000..ebf7c4f48f6 --- /dev/null +++ b/pkg/services/sqlstore/migrator/migrations_test.go @@ -0,0 +1,79 @@ +package migrator + +import ( + _ "embed" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:embed testdata/sqlite_file_migration_statement.sql +var sqliteMigrationStatement string + +func TestConvertUniqueKeyToPrimaryKey(t *testing.T) { + names := []string{ + "drop my_row_id and add primary key with columns path_hash,etag to table file if my_row_id exists (auto-generated mysql column)", + "drop unique index UQE_file_path_hash_etag from file table if it exists (mysql)", + "add primary key with columns path_hash,etag to table file if it doesn't exist (mysql)", + "add primary key with columns path_hash,etag to table file (postgres and sqlite)", + } + expectedMigrations := map[string][]ExpectedMigration{ + MySQL: { + {Id: names[0], SQL: ` + ALTER TABLE file + DROP PRIMARY KEY, + DROP COLUMN my_row_id, + DROP INDEX UQE_file_path_hash_etag, + ADD PRIMARY KEY (` + "`path_hash`" + `,` + "`etag`" + `);`}, + {Id: names[1], SQL: "ALTER TABLE file DROP INDEX UQE_file_path_hash_etag"}, + {Id: names[2], SQL: "ALTER TABLE file ADD PRIMARY KEY (`path_hash`,`etag`)"}, + {Id: names[3], SQL: ""}, + }, + Postgres: { + {Id: names[0], SQL: ""}, + {Id: names[1], SQL: ""}, + {Id: names[2], SQL: ""}, + {Id: names[3], SQL: ` + DO $$ + BEGIN + -- Drop the unique constraint if it exists + DROP INDEX IF EXISTS "UQE_file_path_hash_etag"; + + -- Add primary key if it doesn't already exist + IF NOT EXISTS (SELECT 1 FROM pg_index i WHERE indrelid = 'file'::regclass AND indisprimary) THEN + ALTER TABLE "file" ADD PRIMARY KEY ("path_hash","etag"); + END IF; + END $$;`}, + }, + SQLite: { + {Id: names[0], SQL: ""}, + {Id: names[1], SQL: ""}, + {Id: names[2], SQL: ""}, + {Id: names[3], SQL: sqliteMigrationStatement}, // Embed used here because sqlite statement is full of backquotes. + }, + } + + for dialectName, migrations := range expectedMigrations { + t.Run(dialectName, func(t *testing.T) { + err := CheckExpectedMigrations(dialectName, migrations, func(migrator *Migrator) { + ConvertUniqueKeyToPrimaryKey(migrator, + Index{Cols: []string{"path_hash", "etag"}, Type: UniqueIndex}, // Convert this unique key to primary key + Table{ + Name: "file", + Columns: []*Column{ + {Name: "path", Type: DB_NVarchar, Length: 1024, Nullable: false}, + {Name: "path_hash", Type: DB_NVarchar, Length: 64, Nullable: false, IsPrimaryKey: true}, + {Name: "parent_folder_path_hash", Type: DB_NVarchar, Length: 64, Nullable: false}, + {Name: "contents", Type: DB_Blob, Nullable: false}, + {Name: "etag", Type: DB_NVarchar, Length: 32, Nullable: false, IsPrimaryKey: true}, + }, + PrimaryKeys: []string{"path_hash", "etag"}, + Indices: []*Index{ + {Cols: []string{"parent_folder_path_hash"}}, + }, + }) + }) + require.NoError(t, err) + }) + } +} diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 75ebf8e540b..ca921affdac 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -8,7 +8,6 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate/v4/database" - "github.com/grafana/grafana/pkg/util/sqlite" _ "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" @@ -17,6 +16,8 @@ import ( "go.opentelemetry.io/otel/trace" "go.uber.org/atomic" + "github.com/grafana/grafana/pkg/util/sqlite" + "github.com/grafana/grafana/pkg/util/xorm" "github.com/grafana/grafana/pkg/infra/log" @@ -67,12 +68,16 @@ func NewMigrator(engine *xorm.Engine, cfg *setting.Cfg) *Migrator { // NewScopedMigrator should only be used for the transition to a new storage engine func NewScopedMigrator(engine *xorm.Engine, cfg *setting.Cfg, scope string) *Migrator { + return newMigrator(engine, cfg, scope, NewDialect(engine.DriverName())) +} + +func newMigrator(engine *xorm.Engine, cfg *setting.Cfg, scope string, dialect Dialect) *Migrator { mg := &Migrator{ Cfg: cfg, DBEngine: engine, migrations: make([]Migration, 0), migrationIds: make(map[string]struct{}), - Dialect: NewDialect(engine.DriverName()), + Dialect: dialect, metrics: migratorMetrics{ migCount: prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "grafana_database", @@ -245,7 +250,7 @@ func (mg *Migrator) run(ctx context.Context) (err error) { if !migrationLogExists { // Check if dialect can initialize database from a snapshot. - err := mg.Dialect.CreateDatabaseFromSnapshot(ctx, mg.DBEngine, mg.tableName) + err := mg.Dialect.CreateDatabaseFromSnapshot(ctx, mg.DBEngine, mg.tableName, logger) if err != nil { return fmt.Errorf("failed to create database from snapshot: %w", err) } diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 5a76b4555ca..c356caa8f1c 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -1,15 +1,19 @@ package migrator import ( + "context" "database/sql" + "embed" "errors" "fmt" + "path" "strconv" "strings" "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util/xorm" ) @@ -317,3 +321,87 @@ func (db *MySQLDialect) GetDBName(dsn string) (string, error) { return cfg.DBName, nil } + +//go:embed snapshot/*.sql +var sqlFiles embed.FS + +func (db *MySQLDialect) CreateDatabaseFromSnapshot(ctx context.Context, engine *xorm.Engine, migrationLogTableName string, logger log.Logger) error { + // Entire schema is generated only when called with "migration_log" table. + // Normally if schema creates other "*_migration_log" tables, we don't get here. However if new migration table is introduced by later migration, + // CreateDatabaseFromSnapshot may be called for given table, but if it wasn't part of original schema, we can't do anything here. + if migrationLogTableName != "migration_log" { + return nil + } + + entries, err := sqlFiles.ReadDir("snapshot") + if err != nil { + return err + } + + logger.Info("creating database schema from snapshot") + + for _, entry := range entries { + logger.Debug("using snapshot file", "file", entry.Name()) + data, err := sqlFiles.ReadFile(path.Join("snapshot", entry.Name())) + if err != nil { + return err + } + + statements := extractStatements(string(data)) + if err := db.executeStatements(engine, statements); err != nil { + return err + } + } + + logger.Info("successfully created database schema from snapshot") + return nil +} + +func extractStatements(schema string) []string { + lines := strings.Split(schema, "\n") + + statements := make([]string, 0, len(lines)) + + sb := strings.Builder{} + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "--") { + continue + } + + if line == "" { + continue + } + + endOfStatement := false + if strings.HasSuffix(line, ";") { + endOfStatement = true + line = strings.TrimSuffix(line, ";") + } + + if sb.Len() > 0 { + sb.WriteRune('\n') + } + sb.WriteString(line) + if endOfStatement && sb.Len() > 0 { + statements = append(statements, sb.String()) + sb.Reset() + } + } + + if sb.Len() > 0 { + statements = append(statements, sb.String()) + } + return statements +} + +func (s *MySQLDialect) executeStatements(engine *xorm.Engine, statements []string) error { + sess := engine.NewSession() + for _, s := range statements { + _, err := sess.Exec(s) + if err != nil { + return fmt.Errorf("statement %s failed with error: %v", s, err) + } + } + return nil +} diff --git a/pkg/services/sqlstore/migrator/snapshot/00-schema.sql b/pkg/services/sqlstore/migrator/snapshot/00-schema.sql new file mode 100644 index 00000000000..9907f7083cf --- /dev/null +++ b/pkg/services/sqlstore/migrator/snapshot/00-schema.sql @@ -0,0 +1,2325 @@ +-- MySQL dump 10.13 Distrib 8.4.5, for Linux (x86_64) +-- +-- Host: localhost Database: hg_dump +-- ------------------------------------------------------ +-- Server version 8.4.5 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `alert` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `version` bigint NOT NULL, + `dashboard_id` bigint NOT NULL, + `panel_id` bigint NOT NULL, + `org_id` bigint NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `state` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `settings` mediumtext COLLATE utf8mb4_unicode_ci, + `frequency` bigint NOT NULL, + `handler` bigint NOT NULL, + `severity` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `silenced` tinyint(1) NOT NULL, + `execution_error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `eval_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `eval_date` datetime DEFAULT NULL, + `new_state_date` datetime NOT NULL, + `state_changes` int NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `for` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_alert_org_id_id` (`org_id`,`id`), + KEY `IDX_alert_state` (`state`), + KEY `IDX_alert_dashboard_id` (`dashboard_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_configuration` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_configuration` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `alertmanager_configuration` mediumtext COLLATE utf8mb4_unicode_ci, + `configuration_version` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` int NOT NULL, + `default` tinyint(1) NOT NULL DEFAULT '0', + `org_id` bigint NOT NULL DEFAULT '0', + `configuration_hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'not-yet-calculated', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_configuration_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_configuration_history` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_configuration_history` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL DEFAULT '0', + `alertmanager_configuration` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `configuration_hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'not-yet-calculated', + `configuration_version` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` int NOT NULL, + `default` tinyint(1) NOT NULL DEFAULT '0', + `last_applied` int NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_image` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_image` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `token` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `path` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `url` varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `expires_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_image_token` (`token`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_instance` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_instance` ( + `rule_org_id` bigint NOT NULL, + `rule_uid` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `labels_hash` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `current_state` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `current_state_since` bigint NOT NULL, + `last_eval_time` bigint NOT NULL, + `current_state_end` bigint NOT NULL DEFAULT '0', + `current_reason` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `result_fingerprint` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `resolved_at` bigint DEFAULT NULL, + `last_sent_at` bigint DEFAULT NULL, + `fired_at` bigint DEFAULT NULL, + `annotations` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`rule_org_id`,`rule_uid`,`labels_hash`), + KEY `IDX_alert_instance_rule_org_id_rule_uid_current_state` (`rule_org_id`,`rule_uid`,`current_state`), + KEY `IDX_alert_instance_rule_org_id_current_state` (`rule_org_id`,`current_state`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_notification` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_notification` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `settings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `frequency` bigint DEFAULT NULL, + `send_reminder` tinyint(1) DEFAULT '0', + `disable_resolve_message` tinyint(1) NOT NULL DEFAULT '0', + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `secure_settings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_notification_org_id_uid` (`org_id`,`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_notification_state` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_notification_state` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `alert_id` bigint NOT NULL, + `notifier_id` bigint NOT NULL, + `state` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `version` bigint NOT NULL, + `updated_at` bigint NOT NULL, + `alert_rule_state_updated_version` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_notification_state_org_id_alert_id_notifier_id` (`org_id`,`alert_id`,`notifier_id`), + KEY `IDX_alert_notification_state_alert_id` (`alert_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_rule` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_rule` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `title` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `condition` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` mediumtext COLLATE utf8mb4_unicode_ci, + `updated` datetime NOT NULL, + `interval_seconds` bigint NOT NULL DEFAULT '60', + `version` int NOT NULL DEFAULT '0', + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + `namespace_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `rule_group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `no_data_state` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NoData', + `exec_err_state` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Alerting', + `for` bigint NOT NULL DEFAULT '0', + `annotations` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `panel_id` bigint DEFAULT NULL, + `rule_group_idx` bigint DEFAULT NULL, + `is_paused` tinyint(1) NOT NULL DEFAULT '0', + `notification_settings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `record` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `updated_by` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `keep_firing_for` bigint NOT NULL DEFAULT '0', + `missing_series_evals_to_resolve` smallint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_rule_org_id_uid` (`org_id`,`uid`), + UNIQUE KEY `UQE_alert_rule_guid` (`guid`), + KEY `IDX_alert_rule_org_id_namespace_uid_rule_group` (`org_id`,`namespace_uid`,`rule_group`), + KEY `IDX_alert_rule_org_id_dashboard_uid_panel_id` (`org_id`,`dashboard_uid`,`panel_id`), + KEY `IDX_alert_rule_org_id_namespace_uid_rule_group_rule_group_idx` (`org_id`,`namespace_uid`,`rule_group`,`rule_group_idx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_rule_state` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_rule_state` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `rule_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` longblob NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_rule_state_org_id_rule_uid` (`org_id`,`rule_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_rule_tag` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_rule_tag` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `alert_id` bigint NOT NULL, + `tag_id` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_rule_tag_alert_id_tag_id` (`alert_id`,`tag_id`), + KEY `IDX_alert_rule_tag_alert_id` (`alert_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alert_rule_version` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alert_rule_version` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `rule_org_id` bigint NOT NULL, + `rule_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + `rule_namespace_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `rule_group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `parent_version` int NOT NULL, + `restored_from` int NOT NULL, + `version` int NOT NULL, + `created` datetime NOT NULL, + `title` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `condition` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` mediumtext COLLATE utf8mb4_unicode_ci, + `interval_seconds` bigint NOT NULL, + `no_data_state` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NoData', + `exec_err_state` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Alerting', + `for` bigint NOT NULL DEFAULT '0', + `annotations` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `rule_group_idx` bigint DEFAULT NULL, + `is_paused` tinyint(1) NOT NULL DEFAULT '0', + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `notification_settings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `record` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created_by` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `rule_guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `keep_firing_for` bigint NOT NULL DEFAULT '0', + `missing_series_evals_to_resolve` smallint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_alert_rule_version_rule_org_id_rule_uid_rule_guid_version` (`rule_org_id`,`rule_uid`,`rule_guid`,`version`), + UNIQUE KEY `UQE_alert_rule_version_rule_guid_version` (`rule_guid`,`version`), + KEY `IDX_alert_rule_version_rule_org_id_rule_namespace_uid_rule_group` (`rule_org_id`,`rule_namespace_uid`,`rule_group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `annotation` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `annotation` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `alert_id` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + `dashboard_id` bigint DEFAULT NULL, + `panel_id` bigint DEFAULT NULL, + `category_id` bigint DEFAULT NULL, + `type` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `title` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `metric` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `prev_state` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `new_state` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `epoch` bigint NOT NULL, + `region_id` bigint DEFAULT '0', + `tags` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` bigint DEFAULT '0', + `updated` bigint DEFAULT '0', + `epoch_end` bigint NOT NULL DEFAULT '0', + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_annotation_org_id_alert_id` (`org_id`,`alert_id`), + KEY `IDX_annotation_org_id_type` (`org_id`,`type`), + KEY `IDX_annotation_org_id_created` (`org_id`,`created`), + KEY `IDX_annotation_org_id_updated` (`org_id`,`updated`), + KEY `IDX_annotation_org_id_dashboard_id_epoch_end_epoch` (`org_id`,`dashboard_id`,`epoch_end`,`epoch`), + KEY `IDX_annotation_org_id_epoch_end_epoch` (`org_id`,`epoch_end`,`epoch`), + KEY `IDX_annotation_alert_id` (`alert_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `annotation_tag` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `annotation_tag` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `annotation_id` bigint NOT NULL, + `tag_id` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_annotation_tag_annotation_id_tag_id` (`annotation_id`,`tag_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `anon_device` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `anon_device` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `client_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `device_id` varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `updated_at` datetime NOT NULL, + `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_anon_device_device_id` (`device_id`), + KEY `IDX_anon_device_updated_at` (`updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `api_key` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `api_key` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `expires` bigint DEFAULT NULL, + `service_account_id` bigint DEFAULT NULL, + `last_used_at` datetime DEFAULT NULL, + `is_revoked` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_api_key_key` (`key`), + UNIQUE KEY `UQE_api_key_org_id_name` (`org_id`,`name`), + KEY `IDX_api_key_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `builtin_role` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `builtin_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `role_id` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `org_id` bigint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_builtin_role_org_id_role_id_role` (`org_id`,`role_id`,`role`), + KEY `IDX_builtin_role_role_id` (`role_id`), + KEY `IDX_builtin_role_role` (`role`), + KEY `IDX_builtin_role_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cache_data` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cache_data` ( + `cache_key` varchar(168) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` blob NOT NULL, + `expires` int NOT NULL, + `created_at` int NOT NULL, + PRIMARY KEY (`cache_key`), + UNIQUE KEY `UQE_cache_data_cache_key` (`cache_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cloud_migration_resource` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cloud_migration_resource` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_uid` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `error_string` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `snapshot_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `parent_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `error_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cloud_migration_session` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cloud_migration_session` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `auth_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `slug` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `stack_id` bigint NOT NULL, + `region_slug` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `cluster_slug` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `org_id` bigint NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_cloud_migration_session_uid` (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cloud_migration_snapshot` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cloud_migration_snapshot` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `session_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `finished` datetime DEFAULT NULL, + `upload_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `status` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `local_directory` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `gms_snapshot_uid` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `encryption_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `error_string` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `resource_storage_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `encryption_algo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `metadata` blob, + `public_key` blob, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_cloud_migration_snapshot_uid` (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cloud_migration_snapshot_partition` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cloud_migration_snapshot_partition` ( + `snapshot_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `partition_number` int NOT NULL, + `resource_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` longblob NOT NULL, + PRIMARY KEY (`snapshot_uid`,`resource_type`,`partition_number`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `correlation` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `correlation` ( + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL DEFAULT '0', + `source_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `target_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `label` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `provisioned` tinyint(1) NOT NULL DEFAULT '0', + `type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'query', + PRIMARY KEY (`uid`,`org_id`,`source_uid`), + KEY `IDX_correlation_uid` (`uid`), + KEY `IDX_correlation_source_uid` (`source_uid`), + KEY `IDX_correlation_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `version` int NOT NULL, + `slug` varchar(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `title` varchar(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `updated_by` int DEFAULT NULL, + `created_by` int DEFAULT NULL, + `gnet_id` bigint DEFAULT NULL, + `plugin_id` varchar(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `folder_id` bigint NOT NULL DEFAULT '0', + `is_folder` tinyint(1) NOT NULL DEFAULT '0', + `has_acl` tinyint(1) NOT NULL DEFAULT '0', + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_public` tinyint(1) NOT NULL DEFAULT '0', + `deleted` datetime DEFAULT NULL, + `api_version` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `folder_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_dashboard_org_id_uid` (`org_id`,`uid`), + KEY `IDX_dashboard_org_id` (`org_id`), + KEY `IDX_dashboard_gnet_id` (`gnet_id`), + KEY `IDX_dashboard_org_id_plugin_id` (`org_id`,`plugin_id`), + KEY `IDX_dashboard_title` (`title`), + KEY `IDX_dashboard_is_folder` (`is_folder`), + KEY `IDX_dashboard_deleted` (`deleted`), + KEY `IDX_dashboard_org_id_folder_id_title` (`org_id`,`folder_id`,`title`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_acl` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_acl` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `dashboard_id` bigint NOT NULL, + `user_id` bigint DEFAULT NULL, + `team_id` bigint DEFAULT NULL, + `permission` smallint NOT NULL DEFAULT '4', + `role` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_dashboard_acl_dashboard_id_user_id` (`dashboard_id`,`user_id`), + UNIQUE KEY `UQE_dashboard_acl_dashboard_id_team_id` (`dashboard_id`,`team_id`), + KEY `IDX_dashboard_acl_dashboard_id` (`dashboard_id`), + KEY `IDX_dashboard_acl_user_id` (`user_id`), + KEY `IDX_dashboard_acl_team_id` (`team_id`), + KEY `IDX_dashboard_acl_org_id_role` (`org_id`,`role`), + KEY `IDX_dashboard_acl_permission` (`permission`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_provisioning` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_provisioning` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `dashboard_id` bigint DEFAULT NULL, + `name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `external_id` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `updated` int NOT NULL DEFAULT '0', + `check_sum` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_dashboard_provisioning_dashboard_id` (`dashboard_id`), + KEY `IDX_dashboard_provisioning_dashboard_id_name` (`dashboard_id`,`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_public` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_public` ( + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `time_settings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `template_variables` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `access_token` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_by` int NOT NULL, + `updated_by` int DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime DEFAULT NULL, + `is_enabled` tinyint(1) NOT NULL DEFAULT '0', + `annotations_enabled` tinyint(1) NOT NULL DEFAULT '0', + `time_selection_enabled` tinyint(1) NOT NULL DEFAULT '0', + `share` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public', + PRIMARY KEY (`uid`), + UNIQUE KEY `UQE_dashboard_public_config_uid` (`uid`), + UNIQUE KEY `UQE_dashboard_public_config_access_token` (`access_token`), + KEY `IDX_dashboard_public_config_org_id_dashboard_uid` (`org_id`,`dashboard_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_public_email_share` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_public_email_share` ( + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `public_dashboard_uid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `recipient` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'email', + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_public_magic_link` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_public_magic_link` ( + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `token_uuid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `public_dashboard_uid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_public_session` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_public_session` ( + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `cookie_uuid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `public_dashboard_uid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + `last_seen_at` datetime DEFAULT NULL, + PRIMARY KEY (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_public_usage_by_day` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_public_usage_by_day` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `public_dashboard_uid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `day` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `views` bigint NOT NULL, + `queries` bigint NOT NULL, + `errors` bigint NOT NULL, + `load_duration` float NOT NULL, + `cached_queries` bigint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_snapshot` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_snapshot` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `delete_key` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `external` tinyint(1) NOT NULL, + `external_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `dashboard` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `expires` datetime NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `external_delete_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dashboard_encrypted` mediumblob, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_dashboard_snapshot_key` (`key`), + UNIQUE KEY `UQE_dashboard_snapshot_delete_key` (`delete_key`), + KEY `IDX_dashboard_snapshot_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_tag` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_tag` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `dashboard_id` bigint NOT NULL, + `term` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `org_id` bigint DEFAULT '1', + PRIMARY KEY (`id`), + KEY `IDX_dashboard_tag_dashboard_id` (`dashboard_id`), + KEY `IDX_dashboard_tag_dashboard_uid` (`dashboard_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_usage_by_day` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_usage_by_day` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `dashboard_id` bigint NOT NULL, + `day` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `views` bigint NOT NULL, + `queries` bigint NOT NULL, + `errors` bigint NOT NULL, + `load_duration` float NOT NULL, + `cached_queries` bigint NOT NULL DEFAULT '0', + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `org_id` bigint DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_dashboard_usage_by_day_dashboard_id_day` (`dashboard_id`,`day`), + UNIQUE KEY `UQE_dashboard_usage_by_day_dashboard_uid_org_id_day` (`dashboard_uid`,`org_id`,`day`), + KEY `IDX_dashboard_usage_by_day_dashboard_id` (`dashboard_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_usage_sums` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_usage_sums` ( + `dashboard_id` bigint NOT NULL, + `updated` datetime NOT NULL, + `views_last_1_days` bigint NOT NULL, + `views_last_7_days` bigint NOT NULL, + `views_last_30_days` bigint NOT NULL, + `views_total` bigint NOT NULL, + `queries_last_1_days` bigint NOT NULL, + `queries_last_7_days` bigint NOT NULL, + `queries_last_30_days` bigint NOT NULL, + `queries_total` bigint NOT NULL, + `errors_last_1_days` bigint NOT NULL DEFAULT '0', + `errors_last_7_days` bigint NOT NULL DEFAULT '0', + `errors_last_30_days` bigint NOT NULL DEFAULT '0', + `errors_total` bigint NOT NULL DEFAULT '0', + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `org_id` bigint DEFAULT '1', + PRIMARY KEY (`dashboard_id`), + UNIQUE KEY `UQE_dashboard_usage_sums_org_id_dashboard_uid` (`org_id`,`dashboard_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `dashboard_version` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dashboard_version` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `dashboard_id` bigint NOT NULL, + `parent_version` int NOT NULL, + `restored_from` int NOT NULL, + `version` int NOT NULL, + `created` datetime NOT NULL, + `created_by` bigint NOT NULL, + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` mediumtext COLLATE utf8mb4_unicode_ci, + `api_version` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_dashboard_version_dashboard_id_version` (`dashboard_id`,`version`), + KEY `IDX_dashboard_version_dashboard_id` (`dashboard_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `data_keys` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `data_keys` ( + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `active` tinyint(1) NOT NULL, + `scope` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `provider` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `encrypted_data` blob NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `data_source` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `data_source` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `version` int NOT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `access` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `database` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `basic_auth` tinyint(1) NOT NULL, + `basic_auth_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `basic_auth_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_default` tinyint(1) NOT NULL, + `json_data` mediumtext COLLATE utf8mb4_unicode_ci, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `with_credentials` tinyint(1) NOT NULL DEFAULT '0', + `secure_json_data` mediumtext COLLATE utf8mb4_unicode_ci, + `read_only` tinyint(1) DEFAULT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + `is_prunable` tinyint(1) DEFAULT '0', + `api_version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_data_source_org_id_name` (`org_id`,`name`), + UNIQUE KEY `UQE_data_source_org_id_uid` (`org_id`,`uid`), + KEY `IDX_data_source_org_id` (`org_id`), + KEY `IDX_data_source_org_id_is_default` (`org_id`,`is_default`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `data_source_acl` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `data_source_acl` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `data_source_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `permission` smallint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_data_source_acl_data_source_id_team_id_user_id` (`data_source_id`,`team_id`,`user_id`), + KEY `IDX_data_source_acl_data_source_id` (`data_source_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `data_source_cache` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `data_source_cache` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `data_source_id` bigint NOT NULL, + `enabled` tinyint(1) NOT NULL, + `ttl_ms` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `use_default_ttl` tinyint(1) NOT NULL DEFAULT '1', + `data_source_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + `ttl_resources_ms` bigint NOT NULL DEFAULT '300000', + PRIMARY KEY (`id`), + KEY `IDX_data_source_cache_data_source_id` (`data_source_id`), + KEY `IDX_data_source_cache_data_source_uid` (`data_source_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `data_source_usage_by_day` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `data_source_usage_by_day` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `data_source_id` bigint NOT NULL, + `day` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `queries` bigint NOT NULL, + `errors` bigint NOT NULL, + `load_duration_ms` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_data_source_usage_by_day_data_source_id_day` (`data_source_id`,`day`), + KEY `IDX_data_source_usage_by_day_data_source_id` (`data_source_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `entity_event` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `entity_event` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `entity_id` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `event_type` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` bigint NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `file` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `file` ( + `path` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `path_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `parent_folder_path_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `contents` mediumblob, + `etag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `cache_control` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `content_disposition` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `updated` datetime NOT NULL, + `created` datetime NOT NULL, + `size` bigint NOT NULL, + `mime_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`path_hash`), + KEY `IDX_file_parent_folder_path_hash` (`parent_folder_path_hash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `file_meta` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `file_meta` ( + `path_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`path_hash`,`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `folder` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `folder` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `title` varchar(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parent_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_folder_org_id_uid` (`org_id`,`uid`), + KEY `IDX_folder_org_id_parent_uid` (`org_id`,`parent_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `kv_store` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `kv_store` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `namespace` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_kv_store_org_id_namespace_key` (`org_id`,`namespace`,`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `library_element` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `library_element` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `folder_id` bigint NOT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `kind` bigint NOT NULL, + `type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `model` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `created_by` bigint NOT NULL, + `updated` datetime NOT NULL, + `updated_by` bigint NOT NULL, + `version` bigint NOT NULL, + `folder_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_library_element_org_id_uid` (`org_id`,`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `library_element_connection` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `library_element_connection` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `element_id` bigint NOT NULL, + `kind` bigint NOT NULL, + `connection_id` bigint NOT NULL, + `created` datetime NOT NULL, + `created_by` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_library_element_connection_element_id_kind_connection_id` (`element_id`,`kind`,`connection_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `license_token` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `license_token` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `token` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `login_attempt` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `login_attempt` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `username` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `ip_address` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_login_attempt_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `migration_log` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `migration_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `migration_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `sql` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `success` tinyint(1) NOT NULL, + `error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ngalert_configuration` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ngalert_configuration` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `alertmanagers` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created_at` int NOT NULL, + `updated_at` int NOT NULL, + `send_alerts_to` smallint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_ngalert_configuration_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `org` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `org` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `version` int NOT NULL, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `address1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `address2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `city` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `zip_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `country` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `billing_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_org_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `org_user` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `org_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `role` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_org_user_org_id_user_id` (`org_id`,`user_id`), + KEY `IDX_org_user_org_id` (`org_id`), + KEY `IDX_org_user_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `permission` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `permission` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role_id` bigint NOT NULL, + `action` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `scope` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `kind` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `attribute` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `identifier` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_permission_action_scope_role_id` (`action`,`scope`,`role_id`), + KEY `IDX_permission_identifier` (`identifier`), + KEY `IDX_permission_role_id_action` (`role_id`,`action`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `playlist` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `playlist` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `interval` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `created_at` bigint NOT NULL DEFAULT '0', + `updated_at` bigint NOT NULL DEFAULT '0', + `uid` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_playlist_org_id_uid` (`org_id`,`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `playlist_item` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `playlist_item` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `playlist_id` bigint NOT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `title` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `order` int NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `plugin_setting` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `plugin_setting` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL DEFAULT '1', + `plugin_id` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `enabled` tinyint(1) NOT NULL, + `pinned` tinyint(1) NOT NULL, + `json_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `secure_json_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `plugin_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_plugin_setting_org_id_plugin_id` (`org_id`,`plugin_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `preferences` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `preferences` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `version` int NOT NULL, + `home_dashboard_id` bigint NOT NULL, + `timezone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `theme` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `team_id` bigint DEFAULT NULL, + `week_start` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `json_data` mediumtext COLLATE utf8mb4_unicode_ci, + `home_dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_preferences_org_id` (`org_id`), + KEY `IDX_preferences_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `provenance_type` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `provenance_type` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `record_key` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `record_type` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `provenance` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_provenance_type_record_type_record_key_org_id` (`record_type`,`record_key`,`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `query_history` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `query_history` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `datasource_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_by` bigint DEFAULT NULL, + `created_at` int NOT NULL, + `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `queries` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `IDX_query_history_org_id_created_by_datasource_uid` (`org_id`,`created_by`,`datasource_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `query_history_details` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `query_history_details` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `query_history_item_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `datasource_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `query_history_star` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `query_history_star` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `query_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` bigint DEFAULT NULL, + `org_id` bigint NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_query_history_star_user_id_query_uid` (`user_id`,`query_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `quota` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `quota` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + `target` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `limit` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_quota_org_id_user_id_target` (`org_id`,`user_id`,`target`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `recording_rules` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `recording_rules` ( + `id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `target_ref_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `interval` bigint NOT NULL, + `range` bigint NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '0', + `count` tinyint(1) NOT NULL DEFAULT '0', + `queries` blob NOT NULL, + `created_at` datetime NOT NULL, + `prom_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`,`target_ref_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `remote_write_targets` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `remote_write_targets` ( + `id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data_source_uid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `write_path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` int NOT NULL, + PRIMARY KEY (`id`,`data_source_uid`,`write_path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `report` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `report` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `org_id` bigint NOT NULL, + `dashboard_id` bigint NOT NULL, + `name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `recipients` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `reply_to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `schedule_frequency` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `schedule_day` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `schedule_hour` bigint NOT NULL, + `schedule_minute` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `schedule_timezone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Europe/Stockholm', + `time_from` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `time_to` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pdf_landscape` tinyint(1) DEFAULT NULL, + `schedule_day_of_month` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pdf_layout` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pdf_orientation` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `template_vars` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `enable_dashboard_url` tinyint(1) DEFAULT NULL, + `state` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `enable_csv` tinyint(1) DEFAULT NULL, + `schedule_start` int DEFAULT NULL, + `schedule_end` int DEFAULT NULL, + `schedule_interval_frequency` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `schedule_interval_amount` bigint DEFAULT NULL, + `schedule_workdays_only` tinyint(1) DEFAULT NULL, + `formats` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '["pdf"]', + `scale_factor` smallint NOT NULL DEFAULT '2', + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pdf_show_template_variables` tinyint(1) NOT NULL DEFAULT '0', + `pdf_combine_one_file` tinyint(1) NOT NULL DEFAULT '1', + `subject` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_report_org_id_uid` (`org_id`,`uid`), + KEY `IDX_report_user_id` (`user_id`), + KEY `IDX_report_dashboard_id` (`dashboard_id`), + KEY `IDX_report_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `report_dashboards` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `report_dashboards` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `report_id` bigint NOT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `report_variables` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `time_to` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `time_from` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `IDX_report_dashboards_report_id` (`report_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `report_settings` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `report_settings` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `org_id` bigint NOT NULL, + `branding_report_logo_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `branding_email_logo_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `branding_email_footer_link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `branding_email_footer_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `branding_email_footer_mode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pdf_theme` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'light', + `embedded_image_theme` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dark', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource` ( + `guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_version` bigint DEFAULT NULL, + `group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `action` int NOT NULL, + `label_set` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `previous_resource_version` bigint DEFAULT NULL, + `folder` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + PRIMARY KEY (`guid`), + UNIQUE KEY `UQE_resource_namespace_group_resource_name` (`namespace`,`group`,`resource`,`name`), + KEY `IDX_resource_group_resource` (`group`,`resource`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_blob` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_blob` ( + `uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` longblob NOT NULL, + `hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `content_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`uuid`), + KEY `IDX_resource_history_namespace_group_name` (`namespace`,`group`,`resource`,`name`), + KEY `IDX_resource_blob_created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_events` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_events` ( + `key_path` varchar(2048) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `value` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`key_path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_history` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_history` ( + `guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_version` bigint DEFAULT NULL, + `group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `action` int NOT NULL, + `label_set` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `previous_resource_version` bigint DEFAULT NULL, + `folder` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `generation` bigint NOT NULL DEFAULT '0', + `key_path` varchar(2048) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '', + PRIMARY KEY (`guid`), + UNIQUE KEY `UQE_resource_history_namespace_group_name_version` (`namespace`,`group`,`resource`,`name`,`resource_version`), + KEY `IDX_resource_history_resource_version` (`resource_version`), + KEY `IDX_resource_history_group_resource_resource_version` (`group`,`resource`,`resource_version`), + KEY `IDX_resource_history_namespace_group_resource_action_version` (`namespace`,`group`,`resource`,`action`,`resource_version`), + KEY `IDX_resource_history_namespace_group_resource_name_generation` (`namespace`,`group`,`resource`,`name`,`generation`), + KEY `IDX_resource_history_key_path` (`key_path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_last_import_time` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_last_import_time` ( + `group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `last_import_time` datetime NOT NULL, + PRIMARY KEY (`group`,`resource`,`namespace`), + KEY `UQE_resource_last_import_time_last_import_time` (`last_import_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_migration_log` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_migration_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `migration_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `sql` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `success` tinyint(1) NOT NULL, + `error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `resource_version` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_version` ( + `group` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `resource_version` bigint NOT NULL, + PRIMARY KEY (`group`,`resource`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `role` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `version` bigint NOT NULL, + `org_id` bigint NOT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `display_name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `group_name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `hidden` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_role_org_id_name` (`org_id`,`name`), + UNIQUE KEY `UQE_role_uid` (`uid`), + KEY `IDX_role_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secret_data_key` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secret_data_key` ( + `uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `active` tinyint(1) NOT NULL, + `provider` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `encrypted_data` blob NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`uid`), + KEY `IDX_secret_data_key_namespace_label_active` (`namespace`,`label`,`active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secret_encrypted_value` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secret_encrypted_value` ( + `namespace` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `version` bigint NOT NULL, + `encrypted_data` blob NOT NULL, + `created` bigint NOT NULL, + `updated` bigint NOT NULL, + `data_key_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + PRIMARY KEY (`namespace`,`name`,`version`), + KEY `IDX_secret_encrypted_value_data_key_id` (`data_key_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secret_keeper` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secret_keeper` ( + `guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `annotations` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created` bigint NOT NULL, + `created_by` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `updated` bigint NOT NULL, + `updated_by` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `description` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `active` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`), + UNIQUE KEY `UQE_secret_keeper_namespace_name` (`namespace`,`name`), + KEY `IDX_secret_keeper_namespace_name_active` (`namespace`,`name`,`active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secret_migration_log` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secret_migration_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `migration_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `sql` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `success` tinyint(1) NOT NULL, + `error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secret_secure_value` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secret_secure_value` ( + `guid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `namespace` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `annotations` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created` bigint NOT NULL, + `created_by` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `updated` bigint NOT NULL, + `updated_by` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `external_id` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `active` tinyint(1) NOT NULL, + `version` bigint NOT NULL, + `description` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `keeper` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `decrypters` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `ref` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_reference_api_group` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_reference_api_version` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_reference_kind` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_reference_name` varchar(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `lease_token` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `lease_created` bigint NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`), + UNIQUE KEY `UQE_secret_secure_value_namespace_name_version_active` (`namespace`,`name`,`version`,`active`), + UNIQUE KEY `UQE_secret_secure_value_namespace_name_version` (`namespace`,`name`,`version`), + KEY `IDX_secret_secure_value_namespace_active_updated` (`namespace`,`active`,`updated`), + KEY `IDX_secret_secure_value_lease_token` (`lease_token`), + KEY `IDX_secret_secure_value_lease_created` (`lease_created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `secrets` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `secrets` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `namespace` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `seed_assignment` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `seed_assignment` ( + `id` int NOT NULL AUTO_INCREMENT, + `builtin_role` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `role_name` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `scope` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `origin` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_seed_assignment_builtin_role_role_name` (`builtin_role`,`role_name`), + UNIQUE KEY `UQE_seed_assignment_builtin_role_action_scope` (`builtin_role`,`action`,`scope`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `server_lock` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `server_lock` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `operation_uid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `version` bigint NOT NULL, + `last_execution` bigint NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_server_lock_operation_uid` (`operation_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `session` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `session` ( + `key` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `data` blob NOT NULL, + `expiry` int NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `setting` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `setting` ( + `section` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `encrypted_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`section`,`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `short_url` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `short_url` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `path` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created_by` bigint DEFAULT NULL, + `created_at` int NOT NULL, + `last_seen_at` int DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_short_url_org_id_uid` (`org_id`,`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `signing_key` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `signing_key` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `key_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `private_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `added_at` datetime NOT NULL, + `expires_at` datetime DEFAULT NULL, + `alg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_signing_key_key_id` (`key_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `sso_setting` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sso_setting` ( + `id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `settings` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `is_deleted` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `star` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `star` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `dashboard_id` bigint NOT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `org_id` bigint DEFAULT '1', + `updated` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_star_user_id_dashboard_id` (`user_id`,`dashboard_id`), + UNIQUE KEY `UQE_star_user_id_dashboard_uid_org_id` (`user_id`,`dashboard_uid`,`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `tag` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `tag` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_tag_key_value` (`key`,`value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `team` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `team` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `org_id` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `external_uid` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_provisioned` tinyint(1) DEFAULT NULL, + `email` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_team_org_id_name` (`org_id`,`name`), + UNIQUE KEY `UQE_team_org_id_uid` (`org_id`,`uid`), + KEY `IDX_team_org_id` (`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `team_group` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `team_group` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + `group_id` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_team_group_org_id_team_id_group_id` (`org_id`,`team_id`,`group_id`), + UNIQUE KEY `UQE_team_group_uid` (`uid`), + KEY `IDX_team_group_org_id` (`org_id`), + KEY `IDX_team_group_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `team_member` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `team_member` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `external` tinyint(1) DEFAULT NULL, + `permission` smallint DEFAULT NULL, + `uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_team_member_org_id_team_id_user_id` (`org_id`,`team_id`,`user_id`), + UNIQUE KEY `UQE_team_member_uid` (`uid`), + KEY `IDX_team_member_org_id` (`org_id`), + KEY `IDX_team_member_team_id` (`team_id`), + KEY `IDX_team_member_user_id_org_id` (`user_id`,`org_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `team_role` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `team_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + `role_id` bigint NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_team_role_org_id_team_id_role_id` (`org_id`,`team_id`,`role_id`), + KEY `IDX_team_role_org_id` (`org_id`), + KEY `IDX_team_role_team_id` (`team_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `temp_user` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `temp_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `version` int NOT NULL, + `email` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `role` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `code` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `invited_by_user_id` bigint DEFAULT NULL, + `email_sent` tinyint(1) NOT NULL, + `email_sent_on` datetime DEFAULT NULL, + `remote_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` int NOT NULL DEFAULT '0', + `updated` int NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `IDX_temp_user_email` (`email`), + KEY `IDX_temp_user_org_id` (`org_id`), + KEY `IDX_temp_user_code` (`code`), + KEY `IDX_temp_user_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `test_data` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `test_data` ( + `id` int NOT NULL AUTO_INCREMENT, + `metric1` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `metric2` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `value_big_int` bigint DEFAULT NULL, + `value_double` double DEFAULT NULL, + `value_float` float DEFAULT NULL, + `value_int` int DEFAULT NULL, + `time_epoch` bigint NOT NULL, + `time_date_time` datetime NOT NULL, + `time_time_stamp` timestamp NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `unifiedstorage_migration_log` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `unifiedstorage_migration_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `migration_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `sql` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `success` tinyint(1) NOT NULL, + `error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `version` int NOT NULL, + `login` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `salt` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `rands` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `org_id` bigint NOT NULL, + `is_admin` tinyint(1) NOT NULL, + `email_verified` tinyint(1) DEFAULT NULL, + `theme` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + `help_flags1` bigint NOT NULL DEFAULT '0', + `last_seen_at` datetime DEFAULT NULL, + `is_disabled` tinyint(1) NOT NULL DEFAULT '0', + `is_service_account` tinyint(1) DEFAULT '0', + `uid` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_provisioned` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_user_login` (`login`), + UNIQUE KEY `UQE_user_email` (`email`), + UNIQUE KEY `UQE_user_uid` (`uid`), + KEY `IDX_user_login_email` (`login`,`email`), + KEY `IDX_user_is_service_account_last_seen_at` (`is_service_account`,`last_seen_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_auth` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_auth` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `auth_module` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `auth_id` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created` datetime NOT NULL, + `o_auth_access_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `o_auth_refresh_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `o_auth_token_type` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `o_auth_expiry` datetime DEFAULT NULL, + `o_auth_id_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `external_uid` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + KEY `IDX_user_auth_auth_module_auth_id` (`auth_module`,`auth_id`), + KEY `IDX_user_auth_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_auth_token` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_auth_token` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `auth_token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `prev_auth_token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `client_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `auth_token_seen` tinyint(1) NOT NULL, + `seen_at` int DEFAULT NULL, + `rotated_at` int NOT NULL, + `created_at` int NOT NULL, + `updated_at` int NOT NULL, + `revoked_at` int DEFAULT NULL, + `external_session_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_user_auth_token_auth_token` (`auth_token`), + UNIQUE KEY `UQE_user_auth_token_prev_auth_token` (`prev_auth_token`), + KEY `IDX_user_auth_token_user_id` (`user_id`), + KEY `IDX_user_auth_token_revoked_at` (`revoked_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_dashboard_views` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_dashboard_views` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `dashboard_id` bigint NOT NULL, + `viewed` datetime NOT NULL, + `org_id` bigint DEFAULT NULL, + `dashboard_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_user_dashboard_views_user_id_dashboard_id` (`user_id`,`dashboard_id`), + UNIQUE KEY `UQE_user_dashboard_views_user_id_org_id_dashboard_uid` (`user_id`,`org_id`,`dashboard_uid`), + KEY `IDX_user_dashboard_views_user_id` (`user_id`), + KEY `IDX_user_dashboard_views_dashboard_id` (`dashboard_id`), + KEY `IDX_user_dashboard_views_org_id_dashboard_uid` (`org_id`,`dashboard_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_external_session` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_external_session` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_auth_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `auth_module` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `access_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `refresh_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `session_id` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `session_id_hash` char(44) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name_id` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `name_id_hash` char(44) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `expires_at` datetime DEFAULT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_role` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `org_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `role_id` bigint NOT NULL, + `created` datetime NOT NULL, + `group_mapping_uid` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` (`org_id`,`user_id`,`role_id`,`group_mapping_uid`), + KEY `IDX_user_role_org_id` (`org_id`), + KEY `IDX_user_role_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_stats` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_stats` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `billing_role` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UQE_user_stats_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/pkg/services/sqlstore/migrator/snapshot/96-secret_migration_log.sql b/pkg/services/sqlstore/migrator/snapshot/96-secret_migration_log.sql new file mode 100644 index 00000000000..f5ae4ff6ab8 --- /dev/null +++ b/pkg/services/sqlstore/migrator/snapshot/96-secret_migration_log.sql @@ -0,0 +1,57 @@ +-- MySQL dump 10.13 Distrib 8.4.5, for Linux (x86_64) +-- +-- Host: localhost Database: hg_dump +-- ------------------------------------------------------ +-- Server version 8.4.5 +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `secret_migration_log` +-- + +INSERT INTO `secret_migration_log` (`migration_id`, `sql`, `success`, `error`, `timestamp`) VALUES + ('create secret_migration_log table','CREATE TABLE IF NOT EXISTS `secret_migration_log` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `migration_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `sql` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `success` TINYINT(1) NOT NULL\n, `error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `timestamp` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Initialize secrets tables','',1,'','2022-01-01 00:00:00'), + ('drop table secret_secure_value','DROP TABLE IF EXISTS `secret_secure_value`',1,'','2022-01-01 00:00:00'), + ('create table secret_secure_value','CREATE TABLE IF NOT EXISTS `secret_secure_value` (\n`guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `annotations` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `labels` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` BIGINT(20) NOT NULL\n, `created_by` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` BIGINT(20) NOT NULL\n, `updated_by` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `external_id` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `active` TINYINT(1) NOT NULL\n, `version` BIGINT(20) NOT NULL\n, `description` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `keeper` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `decrypters` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `ref` VARCHAR(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table secret_secure_value, index: 0','CREATE UNIQUE INDEX `UQE_secret_secure_value_namespace_name_version_active` ON `secret_secure_value` (`namespace`,`name`,`version`,`active`);',1,'','2022-01-01 00:00:00'), + ('create table secret_secure_value, index: 1','CREATE UNIQUE INDEX `UQE_secret_secure_value_namespace_name_version` ON `secret_secure_value` (`namespace`,`name`,`version`);',1,'','2022-01-01 00:00:00'), + ('drop table secret_keeper','DROP TABLE IF EXISTS `secret_keeper`',1,'','2022-01-01 00:00:00'), + ('create table secret_keeper','CREATE TABLE IF NOT EXISTS `secret_keeper` (\n`guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `annotations` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `labels` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` BIGINT(20) NOT NULL\n, `created_by` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` BIGINT(20) NOT NULL\n, `updated_by` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `type` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `payload` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table secret_keeper, index: 0','CREATE UNIQUE INDEX `UQE_secret_keeper_namespace_name` ON `secret_keeper` (`namespace`,`name`);',1,'','2022-01-01 00:00:00'), + ('drop table secret_data_key','DROP TABLE IF EXISTS `secret_data_key`',1,'','2022-01-01 00:00:00'), + ('create table secret_data_key','CREATE TABLE IF NOT EXISTS `secret_data_key` (\n`uid` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `namespace` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `label` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `active` TINYINT(1) NOT NULL\n, `provider` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `encrypted_data` BLOB NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('drop table secret_encrypted_value','DROP TABLE IF EXISTS `secret_encrypted_value`',1,'','2022-01-01 00:00:00'), + ('create table secret_encrypted_value','CREATE TABLE IF NOT EXISTS `secret_encrypted_value` (\n`namespace` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `version` BIGINT(20) NOT NULL\n, `encrypted_data` BLOB NOT NULL\n, `created` BIGINT(20) NOT NULL\n, `updated` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table secret_encrypted_value, index: 0','CREATE UNIQUE INDEX `UQE_secret_encrypted_value_namespace_name_version` ON `secret_encrypted_value` (`namespace`,`name`,`version`);',1,'','2022-01-01 00:00:00'), + ('create index for list on secret_secure_value','CREATE INDEX `IDX_secret_secure_value_namespace_active_updated` ON `secret_secure_value` (`namespace`,`active`,`updated`);',1,'','2022-01-01 00:00:00'), + ('create index for list and read current on secret_data_key','CREATE INDEX `IDX_secret_data_key_namespace_label_active` ON `secret_data_key` (`namespace`,`label`,`active`);',1,'','2022-01-01 00:00:00'), + ('add owner_reference_api_group column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `owner_reference_api_group` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add owner_reference_api_version column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `owner_reference_api_version` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add owner_reference_kind column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `owner_reference_kind` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add owner_reference_name column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `owner_reference_name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add lease_token column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `lease_token` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add lease_token index to secret_secure_value','CREATE INDEX `IDX_secret_secure_value_lease_token` ON `secret_secure_value` (`lease_token`);',1,'','2022-01-01 00:00:00'), + ('add lease_created column to secret_secure_value','alter table `secret_secure_value` ADD COLUMN `lease_created` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add lease_created index to secret_secure_value','CREATE INDEX `IDX_secret_secure_value_lease_created` ON `secret_secure_value` (`lease_created`);',1,'','2022-01-01 00:00:00'), + ('add data_key_id column to secret_encrypted_value','alter table `secret_encrypted_value` ADD COLUMN `data_key_id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('add data_key_id index to secret_encrypted_value','CREATE INDEX `IDX_secret_encrypted_value_data_key_id` ON `secret_encrypted_value` (`data_key_id`);',1,'','2022-01-01 00:00:00'), + ('add active column to secret_keeper','alter table `secret_keeper` ADD COLUMN `active` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add active column index to secret_keeper','CREATE INDEX `IDX_secret_keeper_namespace_name_active` ON `secret_keeper` (`namespace`,`name`,`active`);',1,'','2022-01-01 00:00:00'), + ('set secret_secure_value.keeper to \'system\' where keeper is null in secret_secure_value','UPDATE secret_secure_value SET keeper = \'system\' WHERE keeper IS NULL',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key with columns namespace,name,version to table secret_encrypted_value if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE secret_encrypted_value\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_secret_encrypted_value_namespace_name_version,\n ADD PRIMARY KEY (`namespace`,`name`,`version`);\n ',1,'','2022-01-01 00:00:00'), + ('drop unique index UQE_secret_encrypted_value_namespace_name_version from secret_encrypted_value table if it exists (mysql)','ALTER TABLE secret_encrypted_value DROP INDEX UQE_secret_encrypted_value_namespace_name_version',1,'','2022-01-01 00:00:00'), + ('add primary key with columns namespace,name,version to table secret_encrypted_value if it doesn\'t exist (mysql)','ALTER TABLE secret_encrypted_value ADD PRIMARY KEY (`namespace`,`name`,`version`)',1,'','2022-01-01 00:00:00'), + ('add primary key with columns namespace,name,version to table secret_encrypted_value (postgres and sqlite)','',1,'','2022-01-01 00:00:00'); +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/pkg/services/sqlstore/migrator/snapshot/97-resource_migration_log.sql b/pkg/services/sqlstore/migrator/snapshot/97-resource_migration_log.sql new file mode 100644 index 00000000000..35327ae4531 --- /dev/null +++ b/pkg/services/sqlstore/migrator/snapshot/97-resource_migration_log.sql @@ -0,0 +1,60 @@ +-- MySQL dump 10.13 Distrib 8.4.5, for Linux (x86_64) +-- +-- Host: localhost Database: hg_dump +-- ------------------------------------------------------ +-- Server version 8.4.5 +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `resource_migration_log` +-- + +INSERT INTO `resource_migration_log` (`migration_id`, `sql`, `success`, `error`, `timestamp`) VALUES + ('create resource_migration_log table','CREATE TABLE IF NOT EXISTS `resource_migration_log` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `migration_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `sql` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `success` TINYINT(1) NOT NULL\n, `error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `timestamp` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Initialize resource tables','',1,'','2022-01-01 00:00:00'), + ('drop table resource','DROP TABLE IF EXISTS `resource`',1,'','2022-01-01 00:00:00'), + ('create table resource','CREATE TABLE IF NOT EXISTS `resource` (\n`guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `resource_version` BIGINT(20) NULL\n, `group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `action` INT NOT NULL\n, `label_set` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table resource, index: 0','CREATE UNIQUE INDEX `UQE_resource_namespace_group_resource_name` ON `resource` (`namespace`,`group`,`resource`,`name`);',1,'','2022-01-01 00:00:00'), + ('drop table resource_history','DROP TABLE IF EXISTS `resource_history`',1,'','2022-01-01 00:00:00'), + ('create table resource_history','CREATE TABLE IF NOT EXISTS `resource_history` (\n`guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `resource_version` BIGINT(20) NULL\n, `group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `action` INT NOT NULL\n, `label_set` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table resource_history, index: 0','CREATE UNIQUE INDEX `UQE_resource_history_namespace_group_name_version` ON `resource_history` (`namespace`,`group`,`resource`,`name`,`resource_version`);',1,'','2022-01-01 00:00:00'), + ('create table resource_history, index: 1','CREATE INDEX `IDX_resource_history_resource_version` ON `resource_history` (`resource_version`);',1,'','2022-01-01 00:00:00'), + ('drop table resource_version','DROP TABLE IF EXISTS `resource_version`',1,'','2022-01-01 00:00:00'), + ('create table resource_version','CREATE TABLE IF NOT EXISTS `resource_version` (\n`group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource_version` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table resource_version, index: 0','CREATE UNIQUE INDEX `UQE_resource_version_group_resource` ON `resource_version` (`group`,`resource`);',1,'','2022-01-01 00:00:00'), + ('drop table resource_blob','DROP TABLE IF EXISTS `resource_blob`',1,'','2022-01-01 00:00:00'), + ('create table resource_blob','CREATE TABLE IF NOT EXISTS `resource_blob` (\n`uuid` CHAR(36) PRIMARY KEY NOT NULL\n, `created` DATETIME NOT NULL\n, `group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` LONGBLOB NOT NULL\n, `hash` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `content_type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table resource_blob, index: 0','CREATE INDEX `IDX_resource_history_namespace_group_name` ON `resource_blob` (`namespace`,`group`,`resource`,`name`);',1,'','2022-01-01 00:00:00'), + ('create table resource_blob, index: 1','CREATE INDEX `IDX_resource_blob_created` ON `resource_blob` (`created`);',1,'','2022-01-01 00:00:00'), + ('drop table resource_last_import_time','DROP TABLE IF EXISTS `resource_last_import_time`',1,'','2022-01-01 00:00:00'), + ('create table resource_last_import_time','CREATE TABLE IF NOT EXISTS `resource_last_import_time` (\n`group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `namespace` VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `last_import_time` DATETIME NOT NULL\n, PRIMARY KEY ( `group`,`resource`,`namespace` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add column previous_resource_version in resource_history','alter table `resource_history` ADD COLUMN `previous_resource_version` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add column previous_resource_version in resource','alter table `resource` ADD COLUMN `previous_resource_version` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add index to resource_history for polling','CREATE INDEX `IDX_resource_history_group_resource_resource_version` ON `resource_history` (`group`,`resource`,`resource_version`);',1,'','2022-01-01 00:00:00'), + ('Add index to resource for loading','CREATE INDEX `IDX_resource_group_resource` ON `resource` (`group`,`resource`);',1,'','2022-01-01 00:00:00'), + ('Add column folder in resource_history','alter table `resource_history` ADD COLUMN `folder` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('Add column folder in resource','alter table `resource` ADD COLUMN `folder` VARCHAR(253) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('Migrate DeletionMarkers to real Resource objects','Find rows in resource_history with value LIKE {\"kind\":\"DeletedMarker\"%',1,'','2022-01-01 00:00:00'), + ('Add index to resource_history for get trash','CREATE INDEX `IDX_resource_history_namespace_group_resource_action_version` ON `resource_history` (`namespace`,`group`,`resource`,`action`,`resource_version`);',1,'','2022-01-01 00:00:00'), + ('Add generation to resource history','alter table `resource_history` ADD COLUMN `generation` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add generation index to resource history','CREATE INDEX `IDX_resource_history_namespace_group_resource_name_generation` ON `resource_history` (`namespace`,`group`,`resource`,`name`,`generation`);',1,'','2022-01-01 00:00:00'), + ('Add UQE_resource_last_import_time_last_import_time index','CREATE INDEX `UQE_resource_last_import_time_last_import_time` ON `resource_last_import_time` (`last_import_time`);',1,'','2022-01-01 00:00:00'), + ('Add key_path column to resource_history','alter table `resource_history` ADD COLUMN `key_path` VARCHAR(2048) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('create table resource_events','CREATE TABLE IF NOT EXISTS `resource_events` (\n`key_path` VARCHAR(2048) CHARACTER SET latin1 COLLATE latin1_bin PRIMARY KEY NOT NULL\n, `value` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add IDX_resource_history_key_path index','CREATE INDEX `IDX_resource_history_key_path` ON `resource_history` (`key_path`);',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key with columns group,resource to table resource_version if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE resource_version\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_resource_version_group_resource,\n ADD PRIMARY KEY (`group`,`resource`);\n ',1,'','2022-01-01 00:00:00'), + ('drop unique index UQE_resource_version_group_resource from resource_version table if it exists (mysql)','ALTER TABLE resource_version DROP INDEX UQE_resource_version_group_resource',1,'','2022-01-01 00:00:00'), + ('add primary key with columns group,resource to table resource_version if it doesn\'t exist (mysql)','ALTER TABLE resource_version ADD PRIMARY KEY (`group`,`resource`)',1,'','2022-01-01 00:00:00'), + ('add primary key with columns group,resource to table resource_version (postgres and sqlite)','',1,'','2022-01-01 00:00:00'); +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/pkg/services/sqlstore/migrator/snapshot/98-data.sql b/pkg/services/sqlstore/migrator/snapshot/98-data.sql new file mode 100644 index 00000000000..de4d2e04323 --- /dev/null +++ b/pkg/services/sqlstore/migrator/snapshot/98-data.sql @@ -0,0 +1,469 @@ +-- MySQL dump 10.13 Distrib 8.4.5, for Linux (x86_64) +-- +-- Host: localhost Database: hg_dump +-- ------------------------------------------------------ +-- Server version 8.4.5 +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `alert` +-- + + +-- +-- Dumping data for table `alert_image` +-- + + +-- +-- Dumping data for table `alert_instance` +-- + + +-- +-- Dumping data for table `alert_notification` +-- + + +-- +-- Dumping data for table `alert_notification_state` +-- + + +-- +-- Dumping data for table `alert_rule` +-- + + +-- +-- Dumping data for table `alert_rule_state` +-- + + +-- +-- Dumping data for table `alert_rule_tag` +-- + + +-- +-- Dumping data for table `alert_rule_version` +-- + + +-- +-- Dumping data for table `annotation` +-- + + +-- +-- Dumping data for table `annotation_tag` +-- + + +-- +-- Dumping data for table `anon_device` +-- + + +-- +-- Dumping data for table `api_key` +-- + + +-- +-- Dumping data for table `cache_data` +-- + + +-- +-- Dumping data for table `cloud_migration_resource` +-- + + +-- +-- Dumping data for table `cloud_migration_session` +-- + + +-- +-- Dumping data for table `cloud_migration_snapshot` +-- + + +-- +-- Dumping data for table `cloud_migration_snapshot_partition` +-- + + +-- +-- Dumping data for table `correlation` +-- + + +-- +-- Dumping data for table `dashboard` +-- + + +-- +-- Dumping data for table `dashboard_acl` +-- + +INSERT INTO `dashboard_acl` (`id`, `org_id`, `dashboard_id`, `user_id`, `team_id`, `permission`, `role`, `created`, `updated`) VALUES + (1,-1,-1,NULL,NULL,1,'Viewer','2022-01-01 00:00:00','2022-01-01 00:00:00'), + (2,-1,-1,NULL,NULL,2,'Editor','2022-01-01 00:00:00','2022-01-01 00:00:00'); + +-- +-- Dumping data for table `dashboard_provisioning` +-- + + +-- +-- Dumping data for table `dashboard_public` +-- + + +-- +-- Dumping data for table `dashboard_public_email_share` +-- + + +-- +-- Dumping data for table `dashboard_public_magic_link` +-- + + +-- +-- Dumping data for table `dashboard_public_session` +-- + + +-- +-- Dumping data for table `dashboard_public_usage_by_day` +-- + + +-- +-- Dumping data for table `dashboard_snapshot` +-- + + +-- +-- Dumping data for table `dashboard_tag` +-- + + +-- +-- Dumping data for table `dashboard_usage_by_day` +-- + + +-- +-- Dumping data for table `dashboard_usage_sums` +-- + + +-- +-- Dumping data for table `dashboard_version` +-- + + +-- +-- Dumping data for table `data_keys` +-- + + +-- +-- Dumping data for table `data_source` +-- + + +-- +-- Dumping data for table `data_source_acl` +-- + + +-- +-- Dumping data for table `data_source_cache` +-- + + +-- +-- Dumping data for table `data_source_usage_by_day` +-- + + +-- +-- Dumping data for table `entity_event` +-- + + +-- +-- Dumping data for table `file` +-- + + +-- +-- Dumping data for table `file_meta` +-- + + +-- +-- Dumping data for table `folder` +-- + + +-- +-- Dumping data for table `library_element` +-- + + +-- +-- Dumping data for table `library_element_connection` +-- + + +-- +-- Dumping data for table `license_token` +-- + + +-- +-- Dumping data for table `login_attempt` +-- + + +-- +-- Dumping data for table `ngalert_configuration` +-- + + +-- +-- Dumping data for table `playlist` +-- + + +-- +-- Dumping data for table `playlist_item` +-- + + +-- +-- Dumping data for table `plugin_setting` +-- + + +-- +-- Dumping data for table `preferences` +-- + + +-- +-- Dumping data for table `provenance_type` +-- + + +-- +-- Dumping data for table `query_history` +-- + + +-- +-- Dumping data for table `query_history_details` +-- + + +-- +-- Dumping data for table `query_history_star` +-- + + +-- +-- Dumping data for table `quota` +-- + + +-- +-- Dumping data for table `recording_rules` +-- + + +-- +-- Dumping data for table `remote_write_targets` +-- + + +-- +-- Dumping data for table `report` +-- + + +-- +-- Dumping data for table `report_dashboards` +-- + + +-- +-- Dumping data for table `report_settings` +-- + + +-- +-- Dumping data for table `resource` +-- + + +-- +-- Dumping data for table `resource_blob` +-- + + +-- +-- Dumping data for table `resource_events` +-- + + +-- +-- Dumping data for table `resource_history` +-- + + +-- +-- Dumping data for table `secret_data_key` +-- + + +-- +-- Dumping data for table `secret_encrypted_value` +-- + + +-- +-- Dumping data for table `secret_keeper` +-- + + +-- +-- Dumping data for table `secret_secure_value` +-- + + +-- +-- Dumping data for table `secrets` +-- + + +-- +-- Dumping data for table `session` +-- + + +-- +-- Dumping data for table `setting` +-- + + +-- +-- Dumping data for table `short_url` +-- + + +-- +-- Dumping data for table `signing_key` +-- + + +-- +-- Dumping data for table `sso_setting` +-- + + +-- +-- Dumping data for table `star` +-- + + +-- +-- Dumping data for table `tag` +-- + + +-- +-- Dumping data for table `team` +-- + + +-- +-- Dumping data for table `team_group` +-- + + +-- +-- Dumping data for table `team_member` +-- + + +-- +-- Dumping data for table `team_role` +-- + + +-- +-- Dumping data for table `temp_user` +-- + + +-- +-- Dumping data for table `test_data` +-- + + +-- +-- Dumping data for table `unifiedstorage_migration_log` +-- + +INSERT INTO `unifiedstorage_migration_log` (`id`, `migration_id`, `sql`, `success`, `error`, `timestamp`) VALUES + (1,'create unifiedstorage_migration_log table','CREATE TABLE IF NOT EXISTS `unifiedstorage_migration_log` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `migration_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `sql` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `success` TINYINT(1) NOT NULL\n, `error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `timestamp` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + (2,'playlists migration','unified storage data migration: playlists',1,'','2022-01-01 00:00:00'); + +-- +-- Dumping data for table `user_auth` +-- + + +-- +-- Dumping data for table `user_auth_token` +-- + + +-- +-- Dumping data for table `user_dashboard_views` +-- + + +-- +-- Dumping data for table `user_external_session` +-- + + +-- +-- Dumping data for table `user_role` +-- + + +-- +-- Dumping data for table `user_stats` +-- + +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/pkg/services/sqlstore/migrator/snapshot/99-migration_log.sql b/pkg/services/sqlstore/migrator/snapshot/99-migration_log.sql new file mode 100644 index 00000000000..b4900f95f13 --- /dev/null +++ b/pkg/services/sqlstore/migrator/snapshot/99-migration_log.sql @@ -0,0 +1,850 @@ +-- MySQL dump 10.13 Distrib 8.4.5, for Linux (x86_64) +-- +-- Host: localhost Database: hg_dump +-- ------------------------------------------------------ +-- Server version 8.4.5 +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `migration_log` +-- + +INSERT INTO `migration_log` (`migration_id`, `sql`, `success`, `error`, `timestamp`) VALUES + ('create migration_log table','CREATE TABLE IF NOT EXISTS `migration_log` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `migration_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `sql` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `success` TINYINT(1) NOT NULL\n, `error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `timestamp` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create user table','CREATE TABLE IF NOT EXISTS `user` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` INT NOT NULL\n, `login` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `salt` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `rands` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `company` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `account_id` BIGINT(20) NOT NULL\n, `is_admin` TINYINT(1) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index user.login','CREATE UNIQUE INDEX `UQE_user_login` ON `user` (`login`);',1,'','2022-01-01 00:00:00'), + ('add unique index user.email','CREATE UNIQUE INDEX `UQE_user_email` ON `user` (`email`);',1,'','2022-01-01 00:00:00'), + ('drop index UQE_user_login - v1','DROP INDEX `UQE_user_login` ON `user`',1,'','2022-01-01 00:00:00'), + ('drop index UQE_user_email - v1','DROP INDEX `UQE_user_email` ON `user`',1,'','2022-01-01 00:00:00'), + ('Rename table user to user_v1 - v1','ALTER TABLE `user` RENAME TO `user_v1`',1,'','2022-01-01 00:00:00'), + ('create user table v2','CREATE TABLE IF NOT EXISTS `user` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` INT NOT NULL\n, `login` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `salt` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `rands` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `company` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `org_id` BIGINT(20) NOT NULL\n, `is_admin` TINYINT(1) NOT NULL\n, `email_verified` TINYINT(1) NULL\n, `theme` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_user_login - v2','CREATE UNIQUE INDEX `UQE_user_login` ON `user` (`login`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_user_email - v2','CREATE UNIQUE INDEX `UQE_user_email` ON `user` (`email`);',1,'','2022-01-01 00:00:00'), + ('copy data_source v1 to v2','INSERT INTO `user` (`version`\n, `email`\n, `name`\n, `password`\n, `is_admin`\n, `id`\n, `login`\n, `salt`\n, `rands`\n, `company`\n, `org_id`\n, `created`\n, `updated`)\nSELECT `version`\n, `email`\n, `name`\n, `password`\n, `is_admin`\n, `id`\n, `login`\n, `salt`\n, `rands`\n, `company`\n, `account_id`\n, `created`\n, `updated`\nFROM `user_v1`',1,'','2022-01-01 00:00:00'), + ('Drop old table user_v1','DROP TABLE IF EXISTS `user_v1`',1,'','2022-01-01 00:00:00'), + ('Add column help_flags1 to user table','alter table `user` ADD COLUMN `help_flags1` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Update user table charset','ALTER TABLE `user` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `login` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `salt` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `rands` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `company` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `theme` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('Add last_seen_at column to user','alter table `user` ADD COLUMN `last_seen_at` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('Add missing user data','code migration',1,'','2022-01-01 00:00:00'), + ('Add is_disabled column to user','alter table `user` ADD COLUMN `is_disabled` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add index user.login/user.email','CREATE INDEX `IDX_user_login_email` ON `user` (`login`,`email`);',1,'','2022-01-01 00:00:00'), + ('Add is_service_account column to user','alter table `user` ADD COLUMN `is_service_account` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Update is_service_account column to nullable','ALTER TABLE user MODIFY is_service_account BOOLEAN DEFAULT 0;',1,'','2022-01-01 00:00:00'), + ('Add uid column to user','alter table `user` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values for users','UPDATE user SET uid=concat(\'u\',lpad(id,9,\'0\')) WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Make sure users uid are set','UPDATE user SET uid=concat(\'u\',lpad(id,9,\'0\')) WHERE uid is NULL OR uid = \'\';',1,'','2022-01-01 00:00:00'), + ('Add unique index user_uid','CREATE UNIQUE INDEX `UQE_user_uid` ON `user` (`uid`);',1,'','2022-01-01 00:00:00'), + ('Add is_provisioned column to user','alter table `user` ADD COLUMN `is_provisioned` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('update login field with orgid to allow for multiple service accounts with same name across orgs','code migration',1,'','2022-01-01 00:00:00'), + ('update service accounts login field orgid to appear only once','code migration',1,'','2022-01-01 00:00:00'), + ('update login and email fields to lowercase','code migration',1,'','2022-01-01 00:00:00'), + ('update login and email fields to lowercase2','code migration',1,'','2022-01-01 00:00:00'), + ('Add index on user.is_service_account and user.last_seen_at','CREATE INDEX `IDX_user_is_service_account_last_seen_at` ON `user` (`is_service_account`,`last_seen_at`);',1,'','2022-01-01 00:00:00'), + ('Expand user.uid length to 190','ALTER TABLE user MODIFY uid VARCHAR(190);',1,'','2022-01-01 00:00:00'), + ('Prefix SCIM uid for provisioned users','UPDATE user SET uid = CONCAT(\'scim-\', uid) WHERE is_provisioned = 1 AND uid NOT LIKE \'scim-%\';',1,'','2022-01-01 00:00:00'), + ('create temp user table v1-7','CREATE TABLE IF NOT EXISTS `temp_user` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `version` INT NOT NULL\n, `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `code` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `status` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `invited_by_user_id` BIGINT(20) NULL\n, `email_sent` TINYINT(1) NOT NULL\n, `email_sent_on` DATETIME NULL\n, `remote_addr` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_email - v1-7','CREATE INDEX `IDX_temp_user_email` ON `temp_user` (`email`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_org_id - v1-7','CREATE INDEX `IDX_temp_user_org_id` ON `temp_user` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_code - v1-7','CREATE INDEX `IDX_temp_user_code` ON `temp_user` (`code`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_status - v1-7','CREATE INDEX `IDX_temp_user_status` ON `temp_user` (`status`);',1,'','2022-01-01 00:00:00'), + ('Update temp_user table charset','ALTER TABLE `temp_user` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `code` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `status` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `remote_addr` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('drop index IDX_temp_user_email - v1','DROP INDEX `IDX_temp_user_email` ON `temp_user`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_temp_user_org_id - v1','DROP INDEX `IDX_temp_user_org_id` ON `temp_user`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_temp_user_code - v1','DROP INDEX `IDX_temp_user_code` ON `temp_user`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_temp_user_status - v1','DROP INDEX `IDX_temp_user_status` ON `temp_user`',1,'','2022-01-01 00:00:00'), + ('Rename table temp_user to temp_user_tmp_qwerty - v1','ALTER TABLE `temp_user` RENAME TO `temp_user_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create temp_user v2','CREATE TABLE IF NOT EXISTS `temp_user` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `version` INT NOT NULL\n, `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `code` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `status` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `invited_by_user_id` BIGINT(20) NULL\n, `email_sent` TINYINT(1) NOT NULL\n, `email_sent_on` DATETIME NULL\n, `remote_addr` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` INT NOT NULL DEFAULT 0\n, `updated` INT NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_email - v2','CREATE INDEX `IDX_temp_user_email` ON `temp_user` (`email`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_org_id - v2','CREATE INDEX `IDX_temp_user_org_id` ON `temp_user` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_code - v2','CREATE INDEX `IDX_temp_user_code` ON `temp_user` (`code`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_temp_user_status - v2','CREATE INDEX `IDX_temp_user_status` ON `temp_user` (`status`);',1,'','2022-01-01 00:00:00'), + ('copy temp_user v1 to v2','INSERT INTO `temp_user` (`invited_by_user_id`\n, `email_sent`\n, `email_sent_on`\n, `org_id`\n, `version`\n, `email`\n, `name`\n, `code`\n, `status`\n, `remote_addr`\n, `id`\n, `role`)\nSELECT `invited_by_user_id`\n, `email_sent`\n, `email_sent_on`\n, `org_id`\n, `version`\n, `email`\n, `name`\n, `code`\n, `status`\n, `remote_addr`\n, `id`\n, `role`\nFROM `temp_user_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop temp_user_tmp_qwerty','DROP TABLE IF EXISTS `temp_user_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('Set created for temp users that will otherwise prematurely expire','code migration',1,'','2022-01-01 00:00:00'), + ('create star table','CREATE TABLE IF NOT EXISTS `star` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index star.user_id_dashboard_id','CREATE UNIQUE INDEX `UQE_star_user_id_dashboard_id` ON `star` (`user_id`,`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('Add column dashboard_uid in star','alter table `star` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add column org_id in star','alter table `star` ADD COLUMN `org_id` BIGINT(20) NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('Add column updated in star','alter table `star` ADD COLUMN `updated` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('add index in star table on dashboard_uid, org_id and user_id columns','CREATE UNIQUE INDEX `UQE_star_user_id_dashboard_uid_org_id` ON `star` (`user_id`,`dashboard_uid`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('create org table v1','CREATE TABLE IF NOT EXISTS `org` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` INT NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `address1` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `address2` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `city` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `state` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `zip_code` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `country` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `billing_email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_org_name - v1','CREATE UNIQUE INDEX `UQE_org_name` ON `org` (`name`);',1,'','2022-01-01 00:00:00'), + ('create org_user table v1','CREATE TABLE IF NOT EXISTS `org_user` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_org_user_org_id - v1','CREATE INDEX `IDX_org_user_org_id` ON `org_user` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_org_user_org_id_user_id - v1','CREATE UNIQUE INDEX `UQE_org_user_org_id_user_id` ON `org_user` (`org_id`,`user_id`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_org_user_user_id - v1','CREATE INDEX `IDX_org_user_user_id` ON `org_user` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('Update org table charset','ALTER TABLE `org` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `address1` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `address2` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `city` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `state` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `zip_code` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `country` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `billing_email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('Update org_user table charset','ALTER TABLE `org_user` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Migrate all Read Only Viewers to Viewers','UPDATE org_user SET role = \'Viewer\' WHERE role = \'Read Only Editor\'',1,'','2022-01-01 00:00:00'), + ('create dashboard table','CREATE TABLE IF NOT EXISTS `dashboard` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` INT NOT NULL\n, `slug` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `account_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index dashboard.account_id','CREATE INDEX `IDX_dashboard_account_id` ON `dashboard` (`account_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index dashboard_account_id_slug','CREATE UNIQUE INDEX `UQE_dashboard_account_id_slug` ON `dashboard` (`account_id`,`slug`);',1,'','2022-01-01 00:00:00'), + ('create dashboard_tag table','CREATE TABLE IF NOT EXISTS `dashboard_tag` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `term` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index dashboard_tag.dasboard_id_term','CREATE UNIQUE INDEX `UQE_dashboard_tag_dashboard_id_term` ON `dashboard_tag` (`dashboard_id`,`term`);',1,'','2022-01-01 00:00:00'), + ('drop index UQE_dashboard_tag_dashboard_id_term - v1','DROP INDEX `UQE_dashboard_tag_dashboard_id_term` ON `dashboard_tag`',1,'','2022-01-01 00:00:00'), + ('Rename table dashboard to dashboard_v1 - v1','ALTER TABLE `dashboard` RENAME TO `dashboard_v1`',1,'','2022-01-01 00:00:00'), + ('create dashboard v2','CREATE TABLE IF NOT EXISTS `dashboard` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` INT NOT NULL\n, `slug` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_org_id - v2','CREATE INDEX `IDX_dashboard_org_id` ON `dashboard` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_org_id_slug - v2','CREATE UNIQUE INDEX `UQE_dashboard_org_id_slug` ON `dashboard` (`org_id`,`slug`);',1,'','2022-01-01 00:00:00'), + ('copy dashboard v1 to v2','INSERT INTO `dashboard` (`id`\n, `version`\n, `slug`\n, `title`\n, `data`\n, `org_id`\n, `created`\n, `updated`)\nSELECT `id`\n, `version`\n, `slug`\n, `title`\n, `data`\n, `account_id`\n, `created`\n, `updated`\nFROM `dashboard_v1`',1,'','2022-01-01 00:00:00'), + ('drop table dashboard_v1','DROP TABLE IF EXISTS `dashboard_v1`',1,'','2022-01-01 00:00:00'), + ('alter dashboard.data to mediumtext v1','ALTER TABLE dashboard MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Add column updated_by in dashboard - v2','alter table `dashboard` ADD COLUMN `updated_by` INT NULL ',1,'','2022-01-01 00:00:00'), + ('Add column created_by in dashboard - v2','alter table `dashboard` ADD COLUMN `created_by` INT NULL ',1,'','2022-01-01 00:00:00'), + ('Add column gnetId in dashboard','alter table `dashboard` ADD COLUMN `gnet_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add index for gnetId in dashboard','CREATE INDEX `IDX_dashboard_gnet_id` ON `dashboard` (`gnet_id`);',1,'','2022-01-01 00:00:00'), + ('Add column plugin_id in dashboard','alter table `dashboard` ADD COLUMN `plugin_id` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add index for plugin_id in dashboard','CREATE INDEX `IDX_dashboard_org_id_plugin_id` ON `dashboard` (`org_id`,`plugin_id`);',1,'','2022-01-01 00:00:00'), + ('Add index for dashboard_id in dashboard_tag','CREATE INDEX `IDX_dashboard_tag_dashboard_id` ON `dashboard_tag` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('Update dashboard table charset','ALTER TABLE `dashboard` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `slug` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `plugin_id` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `data` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Update dashboard_tag table charset','ALTER TABLE `dashboard_tag` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `term` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add column folder_id in dashboard','alter table `dashboard` ADD COLUMN `folder_id` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add column isFolder in dashboard','alter table `dashboard` ADD COLUMN `is_folder` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add column has_acl in dashboard','alter table `dashboard` ADD COLUMN `has_acl` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add column uid in dashboard','alter table `dashboard` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in dashboard','UPDATE dashboard SET uid=lpad(id,9,\'0\') WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add unique index dashboard_org_id_uid','CREATE UNIQUE INDEX `UQE_dashboard_org_id_uid` ON `dashboard` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index org_id_slug','DROP INDEX `UQE_dashboard_org_id_slug` ON `dashboard`',1,'','2022-01-01 00:00:00'), + ('Update dashboard title length','ALTER TABLE `dashboard` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `title` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add unique index for dashboard_org_id_title_folder_id','CREATE UNIQUE INDEX `UQE_dashboard_org_id_folder_id_title` ON `dashboard` (`org_id`,`folder_id`,`title`);',1,'','2022-01-01 00:00:00'), + ('create dashboard_provisioning','CREATE TABLE IF NOT EXISTS `dashboard_provisioning` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `dashboard_id` BIGINT(20) NULL\n, `name` VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `external_id` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Rename table dashboard_provisioning to dashboard_provisioning_tmp_qwerty - v1','ALTER TABLE `dashboard_provisioning` RENAME TO `dashboard_provisioning_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create dashboard_provisioning v2','CREATE TABLE IF NOT EXISTS `dashboard_provisioning` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `dashboard_id` BIGINT(20) NULL\n, `name` VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `external_id` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` INT NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_provisioning_dashboard_id - v2','CREATE INDEX `IDX_dashboard_provisioning_dashboard_id` ON `dashboard_provisioning` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_provisioning_dashboard_id_name - v2','CREATE INDEX `IDX_dashboard_provisioning_dashboard_id_name` ON `dashboard_provisioning` (`dashboard_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('copy dashboard_provisioning v1 to v2','INSERT INTO `dashboard_provisioning` (`name`\n, `external_id`\n, `id`\n, `dashboard_id`)\nSELECT `name`\n, `external_id`\n, `id`\n, `dashboard_id`\nFROM `dashboard_provisioning_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop dashboard_provisioning_tmp_qwerty','DROP TABLE IF EXISTS `dashboard_provisioning_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('Add check_sum column','alter table `dashboard_provisioning` ADD COLUMN `check_sum` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add index for dashboard_title','CREATE INDEX `IDX_dashboard_title` ON `dashboard` (`title`);',1,'','2022-01-01 00:00:00'), + ('delete tags for deleted dashboards','DELETE FROM dashboard_tag WHERE dashboard_id NOT IN (SELECT id FROM dashboard)',1,'','2022-01-01 00:00:00'), + ('delete stars for deleted dashboards','DELETE FROM star WHERE dashboard_id NOT IN (SELECT id FROM dashboard)',1,'','2022-01-01 00:00:00'), + ('Add index for dashboard_is_folder','CREATE INDEX `IDX_dashboard_is_folder` ON `dashboard` (`is_folder`);',1,'','2022-01-01 00:00:00'), + ('Add isPublic for dashboard','alter table `dashboard` ADD COLUMN `is_public` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add deleted for dashboard','alter table `dashboard` ADD COLUMN `deleted` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('Add index for deleted','CREATE INDEX `IDX_dashboard_deleted` ON `dashboard` (`deleted`);',1,'','2022-01-01 00:00:00'), + ('Add column dashboard_uid in dashboard_tag','alter table `dashboard_tag` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add column org_id in dashboard_tag','alter table `dashboard_tag` ADD COLUMN `org_id` BIGINT(20) NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('Add missing dashboard_uid and org_id to dashboard_tag','code migration',1,'','2022-01-01 00:00:00'), + ('Add apiVersion for dashboard','alter table `dashboard` ADD COLUMN `api_version` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add index for dashboard_uid on dashboard_tag table','CREATE INDEX `IDX_dashboard_tag_dashboard_uid` ON `dashboard_tag` (`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('Add missing dashboard_uid and org_id to star','code migration',1,'','2022-01-01 00:00:00'), + ('create data_source table','CREATE TABLE IF NOT EXISTS `data_source` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `account_id` BIGINT(20) NOT NULL\n, `version` INT NOT NULL\n, `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `access` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `database` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `basic_auth` TINYINT(1) NOT NULL\n, `basic_auth_user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `basic_auth_password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `is_default` TINYINT(1) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index data_source.account_id','CREATE INDEX `IDX_data_source_account_id` ON `data_source` (`account_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index data_source.account_id_name','CREATE UNIQUE INDEX `UQE_data_source_account_id_name` ON `data_source` (`account_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('drop index IDX_data_source_account_id - v1','DROP INDEX `IDX_data_source_account_id` ON `data_source`',1,'','2022-01-01 00:00:00'), + ('drop index UQE_data_source_account_id_name - v1','DROP INDEX `UQE_data_source_account_id_name` ON `data_source`',1,'','2022-01-01 00:00:00'), + ('Rename table data_source to data_source_v1 - v1','ALTER TABLE `data_source` RENAME TO `data_source_v1`',1,'','2022-01-01 00:00:00'), + ('create data_source table v2','CREATE TABLE IF NOT EXISTS `data_source` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `version` INT NOT NULL\n, `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `access` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `database` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `basic_auth` TINYINT(1) NOT NULL\n, `basic_auth_user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `basic_auth_password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `is_default` TINYINT(1) NOT NULL\n, `json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_data_source_org_id - v2','CREATE INDEX `IDX_data_source_org_id` ON `data_source` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_data_source_org_id_name - v2','CREATE UNIQUE INDEX `UQE_data_source_org_id_name` ON `data_source` (`org_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('Drop old table data_source_v1 #2','DROP TABLE IF EXISTS `data_source_v1`',1,'','2022-01-01 00:00:00'), + ('Add column with_credentials','alter table `data_source` ADD COLUMN `with_credentials` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add secure json data column','alter table `data_source` ADD COLUMN `secure_json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update data_source table charset','ALTER TABLE `data_source` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `access` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `database` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `basic_auth_user` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `basic_auth_password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `secure_json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('Update initial version to 1','UPDATE data_source SET version = 1 WHERE version = 0',1,'','2022-01-01 00:00:00'), + ('Add read_only data column','alter table `data_source` ADD COLUMN `read_only` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('Migrate logging ds to loki ds','UPDATE data_source SET type = \'loki\' WHERE type = \'logging\'',1,'','2022-01-01 00:00:00'), + ('Update json_data with nulls','UPDATE data_source SET json_data = \'{}\' WHERE json_data is null',1,'','2022-01-01 00:00:00'), + ('Add uid column','alter table `data_source` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Update uid value','UPDATE data_source SET uid=lpad(id,9,\'0\');',1,'','2022-01-01 00:00:00'), + ('Add unique index datasource_org_id_uid','CREATE UNIQUE INDEX `UQE_data_source_org_id_uid` ON `data_source` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('add unique index datasource_org_id_is_default','CREATE INDEX `IDX_data_source_org_id_is_default` ON `data_source` (`org_id`,`is_default`);',1,'','2022-01-01 00:00:00'), + ('Add is_prunable column','alter table `data_source` ADD COLUMN `is_prunable` TINYINT(1) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add api_version column','alter table `data_source` ADD COLUMN `api_version` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update secure_json_data column to MediumText','ALTER TABLE data_source MODIFY COLUMN secure_json_data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Update json_data column to MediumText','ALTER TABLE data_source MODIFY COLUMN json_data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('create api_key table','CREATE TABLE IF NOT EXISTS `api_key` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `account_id` BIGINT(20) NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `role` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index api_key.account_id','CREATE INDEX `IDX_api_key_account_id` ON `api_key` (`account_id`);',1,'','2022-01-01 00:00:00'), + ('add index api_key.key','CREATE UNIQUE INDEX `UQE_api_key_key` ON `api_key` (`key`);',1,'','2022-01-01 00:00:00'), + ('add index api_key.account_id_name','CREATE UNIQUE INDEX `UQE_api_key_account_id_name` ON `api_key` (`account_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('drop index IDX_api_key_account_id - v1','DROP INDEX `IDX_api_key_account_id` ON `api_key`',1,'','2022-01-01 00:00:00'), + ('drop index UQE_api_key_key - v1','DROP INDEX `UQE_api_key_key` ON `api_key`',1,'','2022-01-01 00:00:00'), + ('drop index UQE_api_key_account_id_name - v1','DROP INDEX `UQE_api_key_account_id_name` ON `api_key`',1,'','2022-01-01 00:00:00'), + ('Rename table api_key to api_key_v1 - v1','ALTER TABLE `api_key` RENAME TO `api_key_v1`',1,'','2022-01-01 00:00:00'), + ('create api_key table v2','CREATE TABLE IF NOT EXISTS `api_key` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `role` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_api_key_org_id - v2','CREATE INDEX `IDX_api_key_org_id` ON `api_key` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_api_key_key - v2','CREATE UNIQUE INDEX `UQE_api_key_key` ON `api_key` (`key`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_api_key_org_id_name - v2','CREATE UNIQUE INDEX `UQE_api_key_org_id_name` ON `api_key` (`org_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('copy api_key v1 to v2','INSERT INTO `api_key` (`name`\n, `key`\n, `role`\n, `created`\n, `updated`\n, `id`\n, `org_id`)\nSELECT `name`\n, `key`\n, `role`\n, `created`\n, `updated`\n, `id`\n, `account_id`\nFROM `api_key_v1`',1,'','2022-01-01 00:00:00'), + ('Drop old table api_key_v1','DROP TABLE IF EXISTS `api_key_v1`',1,'','2022-01-01 00:00:00'), + ('Update api_key table charset','ALTER TABLE `api_key` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `role` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add expires to api_key table','alter table `api_key` ADD COLUMN `expires` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add service account foreign key','alter table `api_key` ADD COLUMN `service_account_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('set service account foreign key to nil if 0','UPDATE api_key SET service_account_id = NULL WHERE service_account_id = 0;',1,'','2022-01-01 00:00:00'), + ('Add last_used_at to api_key table','alter table `api_key` ADD COLUMN `last_used_at` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('Add is_revoked column to api_key table','alter table `api_key` ADD COLUMN `is_revoked` TINYINT(1) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('create dashboard_snapshot table v4','CREATE TABLE IF NOT EXISTS `dashboard_snapshot` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `dashboard` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `expires` DATETIME NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('drop table dashboard_snapshot_v4 #1','DROP TABLE IF EXISTS `dashboard_snapshot`',1,'','2022-01-01 00:00:00'), + ('create dashboard_snapshot table v5 #2','CREATE TABLE IF NOT EXISTS `dashboard_snapshot` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `delete_key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `external` TINYINT(1) NOT NULL\n, `external_url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `dashboard` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `expires` DATETIME NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_snapshot_key - v5','CREATE UNIQUE INDEX `UQE_dashboard_snapshot_key` ON `dashboard_snapshot` (`key`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_snapshot_delete_key - v5','CREATE UNIQUE INDEX `UQE_dashboard_snapshot_delete_key` ON `dashboard_snapshot` (`delete_key`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_snapshot_user_id - v5','CREATE INDEX `IDX_dashboard_snapshot_user_id` ON `dashboard_snapshot` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('alter dashboard_snapshot to mediumtext v2','ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Update dashboard_snapshot table charset','ALTER TABLE `dashboard_snapshot` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `delete_key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `external_url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `dashboard` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add column external_delete_url to dashboard_snapshots table','alter table `dashboard_snapshot` ADD COLUMN `external_delete_url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add encrypted dashboard json column','alter table `dashboard_snapshot` ADD COLUMN `dashboard_encrypted` BLOB NULL ',1,'','2022-01-01 00:00:00'), + ('Change dashboard_encrypted column to MEDIUMBLOB','ALTER TABLE dashboard_snapshot MODIFY dashboard_encrypted MEDIUMBLOB;',1,'','2022-01-01 00:00:00'), + ('create quota table v1','CREATE TABLE IF NOT EXISTS `quota` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NULL\n, `user_id` BIGINT(20) NULL\n, `target` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `limit` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_quota_org_id_user_id_target - v1','CREATE UNIQUE INDEX `UQE_quota_org_id_user_id_target` ON `quota` (`org_id`,`user_id`,`target`);',1,'','2022-01-01 00:00:00'), + ('Update quota table charset','ALTER TABLE `quota` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `target` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('create plugin_setting table','CREATE TABLE IF NOT EXISTS `plugin_setting` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NULL\n, `plugin_id` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `enabled` TINYINT(1) NOT NULL\n, `pinned` TINYINT(1) NOT NULL\n, `json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `secure_json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_plugin_setting_org_id_plugin_id - v1','CREATE UNIQUE INDEX `UQE_plugin_setting_org_id_plugin_id` ON `plugin_setting` (`org_id`,`plugin_id`);',1,'','2022-01-01 00:00:00'), + ('Add column plugin_version to plugin_settings','alter table `plugin_setting` ADD COLUMN `plugin_version` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update plugin_setting table charset','ALTER TABLE `plugin_setting` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `plugin_id` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `secure_json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `plugin_version` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('update NULL org_id to 1','UPDATE plugin_setting SET org_id=1 where org_id IS NULL;',1,'','2022-01-01 00:00:00'), + ('make org_id NOT NULL and DEFAULT VALUE 1','ALTER TABLE plugin_setting MODIFY COLUMN org_id BIGINT NOT NULL DEFAULT 1;',1,'','2022-01-01 00:00:00'), + ('create session table','CREATE TABLE IF NOT EXISTS `session` (\n`key` CHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `data` BLOB NOT NULL\n, `expiry` INTEGER(255) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Drop old table playlist table','DROP TABLE IF EXISTS `playlist`',1,'','2022-01-01 00:00:00'), + ('Drop old table playlist_item table','DROP TABLE IF EXISTS `playlist_item`',1,'','2022-01-01 00:00:00'), + ('create playlist table v2','CREATE TABLE IF NOT EXISTS `playlist` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `interval` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create playlist item table v2','CREATE TABLE IF NOT EXISTS `playlist_item` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `playlist_id` BIGINT(20) NOT NULL\n, `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `title` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `order` INT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Update playlist table charset','ALTER TABLE `playlist` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `interval` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Update playlist_item table charset','ALTER TABLE `playlist_item` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `title` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add playlist column created_at','alter table `playlist` ADD COLUMN `created_at` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add playlist column updated_at','alter table `playlist` ADD COLUMN `updated_at` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('drop preferences table v2','DROP TABLE IF EXISTS `preferences`',1,'','2022-01-01 00:00:00'), + ('drop preferences table v3','DROP TABLE IF EXISTS `preferences`',1,'','2022-01-01 00:00:00'), + ('create preferences table v3','CREATE TABLE IF NOT EXISTS `preferences` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `version` INT NOT NULL\n, `home_dashboard_id` BIGINT(20) NOT NULL\n, `timezone` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `theme` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Update preferences table charset','ALTER TABLE `preferences` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `timezone` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `theme` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add column team_id in preferences','alter table `preferences` ADD COLUMN `team_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Update team_id column values in preferences','UPDATE preferences SET team_id=0 WHERE team_id IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add column week_start in preferences','alter table `preferences` ADD COLUMN `week_start` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add column preferences.json_data','alter table `preferences` ADD COLUMN `json_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('alter preferences.json_data to mediumtext v1','ALTER TABLE preferences MODIFY json_data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Add preferences index org_id','CREATE INDEX `IDX_preferences_org_id` ON `preferences` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('Add preferences index user_id','CREATE INDEX `IDX_preferences_user_id` ON `preferences` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('Add home_dashboard_uid column to preferences table','alter table `preferences` ADD COLUMN `home_dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add missing dashboard_uid to preferences table','code migration',1,'','2022-01-01 00:00:00'), + ('create alert table v1','CREATE TABLE IF NOT EXISTS `alert` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `version` BIGINT(20) NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `panel_id` BIGINT(20) NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `message` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `state` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `frequency` BIGINT(20) NOT NULL\n, `handler` BIGINT(20) NOT NULL\n, `severity` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `silenced` TINYINT(1) NOT NULL\n, `execution_error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `eval_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `eval_date` DATETIME NULL\n, `new_state_date` DATETIME NOT NULL\n, `state_changes` INT NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index alert org_id & id ','CREATE INDEX `IDX_alert_org_id_id` ON `alert` (`org_id`,`id`);',1,'','2022-01-01 00:00:00'), + ('add index alert state','CREATE INDEX `IDX_alert_state` ON `alert` (`state`);',1,'','2022-01-01 00:00:00'), + ('add index alert dashboard_id','CREATE INDEX `IDX_alert_dashboard_id` ON `alert` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('Create alert_rule_tag table v1','CREATE TABLE IF NOT EXISTS `alert_rule_tag` (\n`alert_id` BIGINT(20) NOT NULL\n, `tag_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add unique index alert_rule_tag.alert_id_tag_id','CREATE UNIQUE INDEX `UQE_alert_rule_tag_alert_id_tag_id` ON `alert_rule_tag` (`alert_id`,`tag_id`);',1,'','2022-01-01 00:00:00'), + ('drop index UQE_alert_rule_tag_alert_id_tag_id - v1','DROP INDEX `UQE_alert_rule_tag_alert_id_tag_id` ON `alert_rule_tag`',1,'','2022-01-01 00:00:00'), + ('Rename table alert_rule_tag to alert_rule_tag_v1 - v1','ALTER TABLE `alert_rule_tag` RENAME TO `alert_rule_tag_v1`',1,'','2022-01-01 00:00:00'), + ('Create alert_rule_tag table v2','CREATE TABLE IF NOT EXISTS `alert_rule_tag` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `alert_id` BIGINT(20) NOT NULL\n, `tag_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_alert_rule_tag_alert_id_tag_id - Add unique index alert_rule_tag.alert_id_tag_id V2','CREATE UNIQUE INDEX `UQE_alert_rule_tag_alert_id_tag_id` ON `alert_rule_tag` (`alert_id`,`tag_id`);',1,'','2022-01-01 00:00:00'), + ('copy alert_rule_tag v1 to v2','INSERT INTO `alert_rule_tag` (`alert_id`\n, `tag_id`)\nSELECT `alert_id`\n, `tag_id`\nFROM `alert_rule_tag_v1`',1,'','2022-01-01 00:00:00'), + ('drop table alert_rule_tag_v1','DROP TABLE IF EXISTS `alert_rule_tag_v1`',1,'','2022-01-01 00:00:00'), + ('create alert_notification table v1','CREATE TABLE IF NOT EXISTS `alert_notification` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add column is_default','alter table `alert_notification` ADD COLUMN `is_default` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add column frequency','alter table `alert_notification` ADD COLUMN `frequency` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add column send_reminder','alter table `alert_notification` ADD COLUMN `send_reminder` TINYINT(1) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add column disable_resolve_message','alter table `alert_notification` ADD COLUMN `disable_resolve_message` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add index alert_notification org_id & name','CREATE UNIQUE INDEX `UQE_alert_notification_org_id_name` ON `alert_notification` (`org_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('Update alert table charset','ALTER TABLE `alert` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `message` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `state` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `severity` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `execution_error` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `eval_data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ;',1,'','2022-01-01 00:00:00'), + ('Update alert_notification table charset','ALTER TABLE `alert_notification` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('create notification_journal table v1','CREATE TABLE IF NOT EXISTS `alert_notification_journal` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `alert_id` BIGINT(20) NOT NULL\n, `notifier_id` BIGINT(20) NOT NULL\n, `sent_at` BIGINT(20) NOT NULL\n, `success` TINYINT(1) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index notification_journal org_id & alert_id & notifier_id','CREATE INDEX `IDX_alert_notification_journal_org_id_alert_id_notifier_id` ON `alert_notification_journal` (`org_id`,`alert_id`,`notifier_id`);',1,'','2022-01-01 00:00:00'), + ('drop alert_notification_journal','DROP TABLE IF EXISTS `alert_notification_journal`',1,'','2022-01-01 00:00:00'), + ('create alert_notification_state table v1','CREATE TABLE IF NOT EXISTS `alert_notification_state` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `alert_id` BIGINT(20) NOT NULL\n, `notifier_id` BIGINT(20) NOT NULL\n, `state` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `version` BIGINT(20) NOT NULL\n, `updated_at` BIGINT(20) NOT NULL\n, `alert_rule_state_updated_version` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index alert_notification_state org_id & alert_id & notifier_id','CREATE UNIQUE INDEX `UQE_alert_notification_state_org_id_alert_id_notifier_id` ON `alert_notification_state` (`org_id`,`alert_id`,`notifier_id`);',1,'','2022-01-01 00:00:00'), + ('Add for to alert table','alter table `alert` ADD COLUMN `for` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add column uid in alert_notification','alter table `alert_notification` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in alert_notification','UPDATE alert_notification SET uid=lpad(id,9,\'0\') WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add unique index alert_notification_org_id_uid','CREATE UNIQUE INDEX `UQE_alert_notification_org_id_uid` ON `alert_notification` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index org_id_name','DROP INDEX `UQE_alert_notification_org_id_name` ON `alert_notification`',1,'','2022-01-01 00:00:00'), + ('Add column secure_settings in alert_notification','alter table `alert_notification` ADD COLUMN `secure_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('alter alert.settings to mediumtext','ALTER TABLE alert MODIFY settings MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Add non-unique index alert_notification_state_alert_id','CREATE INDEX `IDX_alert_notification_state_alert_id` ON `alert_notification_state` (`alert_id`);',1,'','2022-01-01 00:00:00'), + ('Add non-unique index alert_rule_tag_alert_id','CREATE INDEX `IDX_alert_rule_tag_alert_id` ON `alert_rule_tag` (`alert_id`);',1,'','2022-01-01 00:00:00'), + ('Drop old annotation table v4','DROP TABLE IF EXISTS `annotation`',1,'','2022-01-01 00:00:00'), + ('create annotation table v5','CREATE TABLE IF NOT EXISTS `annotation` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `alert_id` BIGINT(20) NULL\n, `user_id` BIGINT(20) NULL\n, `dashboard_id` BIGINT(20) NULL\n, `panel_id` BIGINT(20) NULL\n, `category_id` BIGINT(20) NULL\n, `type` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `title` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `text` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `metric` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `prev_state` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `new_state` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `epoch` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index annotation 0 v3','CREATE INDEX `IDX_annotation_org_id_alert_id` ON `annotation` (`org_id`,`alert_id`);',1,'','2022-01-01 00:00:00'), + ('add index annotation 1 v3','CREATE INDEX `IDX_annotation_org_id_type` ON `annotation` (`org_id`,`type`);',1,'','2022-01-01 00:00:00'), + ('add index annotation 2 v3','CREATE INDEX `IDX_annotation_org_id_category_id` ON `annotation` (`org_id`,`category_id`);',1,'','2022-01-01 00:00:00'), + ('add index annotation 3 v3','CREATE INDEX `IDX_annotation_org_id_dashboard_id_panel_id_epoch` ON `annotation` (`org_id`,`dashboard_id`,`panel_id`,`epoch`);',1,'','2022-01-01 00:00:00'), + ('add index annotation 4 v3','CREATE INDEX `IDX_annotation_org_id_epoch` ON `annotation` (`org_id`,`epoch`);',1,'','2022-01-01 00:00:00'), + ('Update annotation table charset','ALTER TABLE `annotation` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `type` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `title` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `text` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `metric` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL , MODIFY `prev_state` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `new_state` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , MODIFY `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add column region_id to annotation table','alter table `annotation` ADD COLUMN `region_id` BIGINT(20) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Drop category_id index','DROP INDEX `IDX_annotation_org_id_category_id` ON `annotation`',1,'','2022-01-01 00:00:00'), + ('Add column tags to annotation table','alter table `annotation` ADD COLUMN `tags` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Create annotation_tag table v2','CREATE TABLE IF NOT EXISTS `annotation_tag` (\n`annotation_id` BIGINT(20) NOT NULL\n, `tag_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add unique index annotation_tag.annotation_id_tag_id','CREATE UNIQUE INDEX `UQE_annotation_tag_annotation_id_tag_id` ON `annotation_tag` (`annotation_id`,`tag_id`);',1,'','2022-01-01 00:00:00'), + ('drop index UQE_annotation_tag_annotation_id_tag_id - v2','DROP INDEX `UQE_annotation_tag_annotation_id_tag_id` ON `annotation_tag`',1,'','2022-01-01 00:00:00'), + ('Rename table annotation_tag to annotation_tag_v2 - v2','ALTER TABLE `annotation_tag` RENAME TO `annotation_tag_v2`',1,'','2022-01-01 00:00:00'), + ('Create annotation_tag table v3','CREATE TABLE IF NOT EXISTS `annotation_tag` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `annotation_id` BIGINT(20) NOT NULL\n, `tag_id` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_annotation_tag_annotation_id_tag_id - Add unique index annotation_tag.annotation_id_tag_id V3','CREATE UNIQUE INDEX `UQE_annotation_tag_annotation_id_tag_id` ON `annotation_tag` (`annotation_id`,`tag_id`);',1,'','2022-01-01 00:00:00'), + ('copy annotation_tag v2 to v3','INSERT INTO `annotation_tag` (`annotation_id`\n, `tag_id`)\nSELECT `annotation_id`\n, `tag_id`\nFROM `annotation_tag_v2`',1,'','2022-01-01 00:00:00'), + ('drop table annotation_tag_v2','DROP TABLE IF EXISTS `annotation_tag_v2`',1,'','2022-01-01 00:00:00'), + ('Update alert annotations and set TEXT to empty','UPDATE annotation SET TEXT = \'\' WHERE alert_id > 0',1,'','2022-01-01 00:00:00'), + ('Add created time to annotation table','alter table `annotation` ADD COLUMN `created` BIGINT(20) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add updated time to annotation table','alter table `annotation` ADD COLUMN `updated` BIGINT(20) NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add index for created in annotation table','CREATE INDEX `IDX_annotation_org_id_created` ON `annotation` (`org_id`,`created`);',1,'','2022-01-01 00:00:00'), + ('Add index for updated in annotation table','CREATE INDEX `IDX_annotation_org_id_updated` ON `annotation` (`org_id`,`updated`);',1,'','2022-01-01 00:00:00'), + ('Convert existing annotations from seconds to milliseconds','UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999',1,'','2022-01-01 00:00:00'), + ('Add epoch_end column','alter table `annotation` ADD COLUMN `epoch_end` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add index for epoch_end','CREATE INDEX `IDX_annotation_org_id_epoch_epoch_end` ON `annotation` (`org_id`,`epoch`,`epoch_end`);',1,'','2022-01-01 00:00:00'), + ('Make epoch_end the same as epoch','UPDATE annotation SET epoch_end = epoch',1,'','2022-01-01 00:00:00'), + ('Move region to single row','code migration',1,'','2022-01-01 00:00:00'), + ('Remove index org_id_epoch from annotation table','DROP INDEX `IDX_annotation_org_id_epoch` ON `annotation`',1,'','2022-01-01 00:00:00'), + ('Remove index org_id_dashboard_id_panel_id_epoch from annotation table','DROP INDEX `IDX_annotation_org_id_dashboard_id_panel_id_epoch` ON `annotation`',1,'','2022-01-01 00:00:00'), + ('Add index for org_id_dashboard_id_epoch_end_epoch on annotation table','CREATE INDEX `IDX_annotation_org_id_dashboard_id_epoch_end_epoch` ON `annotation` (`org_id`,`dashboard_id`,`epoch_end`,`epoch`);',1,'','2022-01-01 00:00:00'), + ('Add index for org_id_epoch_end_epoch on annotation table','CREATE INDEX `IDX_annotation_org_id_epoch_end_epoch` ON `annotation` (`org_id`,`epoch_end`,`epoch`);',1,'','2022-01-01 00:00:00'), + ('Remove index org_id_epoch_epoch_end from annotation table','DROP INDEX `IDX_annotation_org_id_epoch_epoch_end` ON `annotation`',1,'','2022-01-01 00:00:00'), + ('Add index for alert_id on annotation table','CREATE INDEX `IDX_annotation_alert_id` ON `annotation` (`alert_id`);',1,'','2022-01-01 00:00:00'), + ('Increase tags column to length 4096','ALTER TABLE annotation MODIFY tags VARCHAR(4096);',1,'','2022-01-01 00:00:00'), + ('Increase prev_state column to length 40 not null','ALTER TABLE annotation MODIFY prev_state VARCHAR(40) NOT NULL;',1,'','2022-01-01 00:00:00'), + ('Increase new_state column to length 40 not null','ALTER TABLE annotation MODIFY new_state VARCHAR(40) NOT NULL;',1,'','2022-01-01 00:00:00'), + ('Add dashboard_uid column to annotation table','alter table `annotation` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add missing dashboard_uid to annotation table','code migration',1,'','2022-01-01 00:00:00'), + ('create test_data table','CREATE TABLE IF NOT EXISTS `test_data` (\n`id` INT PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `metric1` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `metric2` VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `value_big_int` BIGINT(20) NULL\n, `value_double` DOUBLE NULL\n, `value_float` FLOAT NULL\n, `value_int` INT NULL\n, `time_epoch` BIGINT(20) NOT NULL\n, `time_date_time` DATETIME NOT NULL\n, `time_time_stamp` TIMESTAMP NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create dashboard_version table v1','CREATE TABLE IF NOT EXISTS `dashboard_version` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `parent_version` INT NOT NULL\n, `restored_from` INT NOT NULL\n, `version` INT NOT NULL\n, `created` DATETIME NOT NULL\n, `created_by` BIGINT(20) NOT NULL\n, `message` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index dashboard_version.dashboard_id','CREATE INDEX `IDX_dashboard_version_dashboard_id` ON `dashboard_version` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index dashboard_version.dashboard_id and dashboard_version.version','CREATE UNIQUE INDEX `UQE_dashboard_version_dashboard_id_version` ON `dashboard_version` (`dashboard_id`,`version`);',1,'','2022-01-01 00:00:00'), + ('Set dashboard version to 1 where 0','UPDATE dashboard SET version = 1 WHERE version = 0',1,'','2022-01-01 00:00:00'), + ('save existing dashboard data in dashboard_version table v1','INSERT INTO dashboard_version\n(\n dashboard_id,\n version,\n parent_version,\n restored_from,\n created,\n created_by,\n message,\n data\n)\nSELECT\n dashboard.id,\n dashboard.version,\n dashboard.version,\n dashboard.version,\n dashboard.updated,\n COALESCE(dashboard.updated_by, -1),\n \'\',\n dashboard.data\nFROM dashboard;',1,'','2022-01-01 00:00:00'), + ('alter dashboard_version.data to mediumtext v1','ALTER TABLE dashboard_version MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('Add apiVersion for dashboard_version','alter table `dashboard_version` ADD COLUMN `api_version` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('create team table','CREATE TABLE IF NOT EXISTS `team` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index team.org_id','CREATE INDEX `IDX_team_org_id` ON `team` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index team_org_id_name','CREATE UNIQUE INDEX `UQE_team_org_id_name` ON `team` (`org_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('Add column uid in team','alter table `team` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in team','UPDATE team SET uid=concat(\'t\',lpad(id,9,\'0\')) WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add unique index team_org_id_uid','CREATE UNIQUE INDEX `UQE_team_org_id_uid` ON `team` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Add column external_uid in team','alter table `team` ADD COLUMN `external_uid` VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add column is_provisioned in team','alter table `team` ADD COLUMN `is_provisioned` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('create team member table','CREATE TABLE IF NOT EXISTS `team_member` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `team_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index team_member.org_id','CREATE INDEX `IDX_team_member_org_id` ON `team_member` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index team_member_org_id_team_id_user_id','CREATE UNIQUE INDEX `UQE_team_member_org_id_team_id_user_id` ON `team_member` (`org_id`,`team_id`,`user_id`);',1,'','2022-01-01 00:00:00'), + ('add index team_member.team_id','CREATE INDEX `IDX_team_member_team_id` ON `team_member` (`team_id`);',1,'','2022-01-01 00:00:00'), + ('Add column email to team table','alter table `team` ADD COLUMN `email` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add column external to team_member table','alter table `team_member` ADD COLUMN `external` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('Add column permission to team_member table','alter table `team_member` ADD COLUMN `permission` SMALLINT NULL ',1,'','2022-01-01 00:00:00'), + ('add unique index team_member_user_id_org_id','CREATE INDEX `IDX_team_member_user_id_org_id` ON `team_member` (`user_id`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('Add column uid in team_member','alter table `team_member` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in team_member','UPDATE team_member SET uid=concat(\'tm\',lpad(id,9,\'0\')) WHERE uid IS NULL OR uid = \'\';',1,'','2022-01-01 00:00:00'), + ('Add unique index team_member_uid','CREATE UNIQUE INDEX `UQE_team_member_uid` ON `team_member` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create dashboard acl table','CREATE TABLE IF NOT EXISTS `dashboard_acl` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NULL\n, `team_id` BIGINT(20) NULL\n, `permission` SMALLINT NOT NULL DEFAULT 4\n, `role` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index dashboard_acl_dashboard_id','CREATE INDEX `IDX_dashboard_acl_dashboard_id` ON `dashboard_acl` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index dashboard_acl_dashboard_id_user_id','CREATE UNIQUE INDEX `UQE_dashboard_acl_dashboard_id_user_id` ON `dashboard_acl` (`dashboard_id`,`user_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index dashboard_acl_dashboard_id_team_id','CREATE UNIQUE INDEX `UQE_dashboard_acl_dashboard_id_team_id` ON `dashboard_acl` (`dashboard_id`,`team_id`);',1,'','2022-01-01 00:00:00'), + ('add index dashboard_acl_user_id','CREATE INDEX `IDX_dashboard_acl_user_id` ON `dashboard_acl` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('add index dashboard_acl_team_id','CREATE INDEX `IDX_dashboard_acl_team_id` ON `dashboard_acl` (`team_id`);',1,'','2022-01-01 00:00:00'), + ('add index dashboard_acl_org_id_role','CREATE INDEX `IDX_dashboard_acl_org_id_role` ON `dashboard_acl` (`org_id`,`role`);',1,'','2022-01-01 00:00:00'), + ('add index dashboard_permission','CREATE INDEX `IDX_dashboard_acl_permission` ON `dashboard_acl` (`permission`);',1,'','2022-01-01 00:00:00'), + ('save default acl rules in dashboard_acl table','\nINSERT INTO dashboard_acl\n (\n org_id,\n dashboard_id,\n permission,\n role,\n created,\n updated\n )\n VALUES\n (-1,-1, 1,\'Viewer\',\'2017-06-20\',\'2017-06-20\'),\n (-1,-1, 2,\'Editor\',\'2017-06-20\',\'2017-06-20\')\n ',1,'','2022-01-01 00:00:00'), + ('delete acl rules for deleted dashboards and folders','DELETE FROM dashboard_acl WHERE dashboard_id NOT IN (SELECT id FROM dashboard) AND dashboard_id != -1',1,'','2022-01-01 00:00:00'), + ('create tag table','CREATE TABLE IF NOT EXISTS `tag` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `key` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index tag.key_value','CREATE UNIQUE INDEX `UQE_tag_key_value` ON `tag` (`key`,`value`);',1,'','2022-01-01 00:00:00'), + ('create login attempt table','CREATE TABLE IF NOT EXISTS `login_attempt` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `username` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `ip_address` VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index login_attempt.username','CREATE INDEX `IDX_login_attempt_username` ON `login_attempt` (`username`);',1,'','2022-01-01 00:00:00'), + ('drop index IDX_login_attempt_username - v1','DROP INDEX `IDX_login_attempt_username` ON `login_attempt`',1,'','2022-01-01 00:00:00'), + ('Rename table login_attempt to login_attempt_tmp_qwerty - v1','ALTER TABLE `login_attempt` RENAME TO `login_attempt_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create login_attempt v2','CREATE TABLE IF NOT EXISTS `login_attempt` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `username` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `ip_address` VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` INT NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_login_attempt_username - v2','CREATE INDEX `IDX_login_attempt_username` ON `login_attempt` (`username`);',1,'','2022-01-01 00:00:00'), + ('copy login_attempt v1 to v2','INSERT INTO `login_attempt` (`id`\n, `username`\n, `ip_address`)\nSELECT `id`\n, `username`\n, `ip_address`\nFROM `login_attempt_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop login_attempt_tmp_qwerty','DROP TABLE IF EXISTS `login_attempt_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('increase login_attempt.ip_address column length for IPv6 support','ALTER TABLE login_attempt MODIFY ip_address VARCHAR(50);',1,'','2022-01-01 00:00:00'), + ('alter table login_attempt alter column created type to bigint','ALTER TABLE login_attempt MODIFY created BIGINT;',1,'','2022-01-01 00:00:00'), + ('create user auth table','CREATE TABLE IF NOT EXISTS `user_auth` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `auth_module` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `auth_id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_user_auth_auth_module_auth_id - v1','CREATE INDEX `IDX_user_auth_auth_module_auth_id` ON `user_auth` (`auth_module`,`auth_id`);',1,'','2022-01-01 00:00:00'), + ('alter user_auth.auth_id to length 190','ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);',1,'','2022-01-01 00:00:00'), + ('Add OAuth access token to user_auth','alter table `user_auth` ADD COLUMN `o_auth_access_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add OAuth refresh token to user_auth','alter table `user_auth` ADD COLUMN `o_auth_refresh_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add OAuth token type to user_auth','alter table `user_auth` ADD COLUMN `o_auth_token_type` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add OAuth expiry to user_auth','alter table `user_auth` ADD COLUMN `o_auth_expiry` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('Add index to user_id column in user_auth','CREATE INDEX `IDX_user_auth_user_id` ON `user_auth` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('Add OAuth ID token to user_auth','alter table `user_auth` ADD COLUMN `o_auth_id_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add user_unique_id to user_auth','alter table `user_auth` ADD COLUMN `external_uid` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('create server_lock table','CREATE TABLE IF NOT EXISTS `server_lock` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `operation_uid` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `version` BIGINT(20) NOT NULL\n, `last_execution` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index server_lock.operation_uid','CREATE UNIQUE INDEX `UQE_server_lock_operation_uid` ON `server_lock` (`operation_uid`);',1,'','2022-01-01 00:00:00'), + ('create user auth token table','CREATE TABLE IF NOT EXISTS `user_auth_token` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `auth_token` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `prev_auth_token` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `user_agent` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `client_ip` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `auth_token_seen` TINYINT(1) NOT NULL\n, `seen_at` INT NULL\n, `rotated_at` INT NOT NULL\n, `created_at` INT NOT NULL\n, `updated_at` INT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index user_auth_token.auth_token','CREATE UNIQUE INDEX `UQE_user_auth_token_auth_token` ON `user_auth_token` (`auth_token`);',1,'','2022-01-01 00:00:00'), + ('add unique index user_auth_token.prev_auth_token','CREATE UNIQUE INDEX `UQE_user_auth_token_prev_auth_token` ON `user_auth_token` (`prev_auth_token`);',1,'','2022-01-01 00:00:00'), + ('add index user_auth_token.user_id','CREATE INDEX `IDX_user_auth_token_user_id` ON `user_auth_token` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('Add revoked_at to the user auth token','alter table `user_auth_token` ADD COLUMN `revoked_at` INT NULL ',1,'','2022-01-01 00:00:00'), + ('add index user_auth_token.revoked_at','CREATE INDEX `IDX_user_auth_token_revoked_at` ON `user_auth_token` (`revoked_at`);',1,'','2022-01-01 00:00:00'), + ('add external_session_id to user_auth_token','alter table `user_auth_token` ADD COLUMN `external_session_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('create cache_data table','CREATE TABLE IF NOT EXISTS `cache_data` (\n`cache_key` VARCHAR(168) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `data` BLOB NOT NULL\n, `expires` INTEGER(255) NOT NULL\n, `created_at` INTEGER(255) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index cache_data.cache_key','CREATE UNIQUE INDEX `UQE_cache_data_cache_key` ON `cache_data` (`cache_key`);',1,'','2022-01-01 00:00:00'), + ('create short_url table v1','CREATE TABLE IF NOT EXISTS `short_url` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_by` INT NOT NULL\n, `created_at` INT NOT NULL\n, `last_seen_at` INT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index short_url.org_id-uid','CREATE UNIQUE INDEX `UQE_short_url_org_id_uid` ON `short_url` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('alter table short_url alter column created_by type to bigint','ALTER TABLE short_url MODIFY created_by BIGINT;',1,'','2022-01-01 00:00:00'), + ('delete alert_definition table','DROP TABLE IF EXISTS `alert_definition`',1,'','2022-01-01 00:00:00'), + ('recreate alert_definition table','CREATE TABLE IF NOT EXISTS `alert_definition` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `title` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `condition` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` DATETIME NOT NULL\n, `interval_seconds` BIGINT(20) NOT NULL DEFAULT 60\n, `version` INT NOT NULL DEFAULT 0\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in alert_definition on org_id and title columns','CREATE INDEX `IDX_alert_definition_org_id_title` ON `alert_definition` (`org_id`,`title`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_definition on org_id and uid columns','CREATE INDEX `IDX_alert_definition_org_id_uid` ON `alert_definition` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('alter alert_definition table data column to mediumtext in mysql','ALTER TABLE alert_definition MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('drop index in alert_definition on org_id and title columns','DROP INDEX `IDX_alert_definition_org_id_title` ON `alert_definition`',1,'','2022-01-01 00:00:00'), + ('drop index in alert_definition on org_id and uid columns','DROP INDEX `IDX_alert_definition_org_id_uid` ON `alert_definition`',1,'','2022-01-01 00:00:00'), + ('add unique index in alert_definition on org_id and title columns','CREATE UNIQUE INDEX `UQE_alert_definition_org_id_title` ON `alert_definition` (`org_id`,`title`);',1,'','2022-01-01 00:00:00'), + ('add unique index in alert_definition on org_id and uid columns','CREATE UNIQUE INDEX `UQE_alert_definition_org_id_uid` ON `alert_definition` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Add column paused in alert_definition','alter table `alert_definition` ADD COLUMN `paused` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('drop alert_definition table','DROP TABLE IF EXISTS `alert_definition`',1,'','2022-01-01 00:00:00'), + ('delete alert_definition_version table','DROP TABLE IF EXISTS `alert_definition_version`',1,'','2022-01-01 00:00:00'), + ('recreate alert_definition_version table','CREATE TABLE IF NOT EXISTS `alert_definition_version` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `alert_definition_id` BIGINT(20) NOT NULL\n, `alert_definition_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0\n, `parent_version` INT NOT NULL\n, `restored_from` INT NOT NULL\n, `version` INT NOT NULL\n, `created` DATETIME NOT NULL\n, `title` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `condition` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `interval_seconds` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in alert_definition_version table on alert_definition_id and version columns','CREATE UNIQUE INDEX `UQE_alert_definition_version_alert_definition_id_version` ON `alert_definition_version` (`alert_definition_id`,`version`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_definition_version table on alert_definition_uid and version columns','CREATE UNIQUE INDEX `UQE_alert_definition_version_alert_definition_uid_version` ON `alert_definition_version` (`alert_definition_uid`,`version`);',1,'','2022-01-01 00:00:00'), + ('alter alert_definition_version table data column to mediumtext in mysql','ALTER TABLE alert_definition_version MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('drop alert_definition_version table','DROP TABLE IF EXISTS `alert_definition_version`',1,'','2022-01-01 00:00:00'), + ('create alert_instance table','CREATE TABLE IF NOT EXISTS `alert_instance` (\n`def_org_id` BIGINT(20) NOT NULL\n, `def_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0\n, `labels` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `labels_hash` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `current_state` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `current_state_since` BIGINT(20) NOT NULL\n, `last_eval_time` BIGINT(20) NOT NULL\n, PRIMARY KEY ( `def_org_id`,`def_uid`,`labels_hash` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in alert_instance table on def_org_id, def_uid and current_state columns','CREATE INDEX `IDX_alert_instance_def_org_id_def_uid_current_state` ON `alert_instance` (`def_org_id`,`def_uid`,`current_state`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_instance table on def_org_id, current_state columns','CREATE INDEX `IDX_alert_instance_def_org_id_current_state` ON `alert_instance` (`def_org_id`,`current_state`);',1,'','2022-01-01 00:00:00'), + ('add column current_state_end to alert_instance','alter table `alert_instance` ADD COLUMN `current_state_end` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('remove index def_org_id, def_uid, current_state on alert_instance','DROP INDEX `IDX_alert_instance_def_org_id_def_uid_current_state` ON `alert_instance`',1,'','2022-01-01 00:00:00'), + ('remove index def_org_id, current_state on alert_instance','DROP INDEX `IDX_alert_instance_def_org_id_current_state` ON `alert_instance`',1,'','2022-01-01 00:00:00'), + ('rename def_org_id to rule_org_id in alert_instance','ALTER TABLE alert_instance CHANGE def_org_id rule_org_id BIGINT;',1,'','2022-01-01 00:00:00'), + ('rename def_uid to rule_uid in alert_instance','ALTER TABLE alert_instance CHANGE def_uid rule_uid VARCHAR(40);',1,'','2022-01-01 00:00:00'), + ('add index rule_org_id, rule_uid, current_state on alert_instance','CREATE INDEX `IDX_alert_instance_rule_org_id_rule_uid_current_state` ON `alert_instance` (`rule_org_id`,`rule_uid`,`current_state`);',1,'','2022-01-01 00:00:00'), + ('add index rule_org_id, current_state on alert_instance','CREATE INDEX `IDX_alert_instance_rule_org_id_current_state` ON `alert_instance` (`rule_org_id`,`current_state`);',1,'','2022-01-01 00:00:00'), + ('add current_reason column related to current_state','alter table `alert_instance` ADD COLUMN `current_reason` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add result_fingerprint column to alert_instance','alter table `alert_instance` ADD COLUMN `result_fingerprint` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('create alert_rule table','CREATE TABLE IF NOT EXISTS `alert_rule` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `title` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `condition` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` DATETIME NOT NULL\n, `interval_seconds` BIGINT(20) NOT NULL DEFAULT 60\n, `version` INT NOT NULL DEFAULT 0\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0\n, `namespace_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `rule_group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `no_data_state` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'NoData\'\n, `exec_err_state` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'Alerting\'\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id and title columns','CREATE UNIQUE INDEX `UQE_alert_rule_org_id_title` ON `alert_rule` (`org_id`,`title`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id and uid columns','CREATE UNIQUE INDEX `UQE_alert_rule_org_id_uid` ON `alert_rule` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id, namespace_uid, group_uid columns','CREATE INDEX `IDX_alert_rule_org_id_namespace_uid_rule_group` ON `alert_rule` (`org_id`,`namespace_uid`,`rule_group`);',1,'','2022-01-01 00:00:00'), + ('alter alert_rule table data column to mediumtext in mysql','ALTER TABLE alert_rule MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('add column for to alert_rule','alter table `alert_rule` ADD COLUMN `for` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column annotations to alert_rule','alter table `alert_rule` ADD COLUMN `annotations` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add column labels to alert_rule','alter table `alert_rule` ADD COLUMN `labels` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('remove unique index from alert_rule on org_id, title columns','DROP INDEX `UQE_alert_rule_org_id_title` ON `alert_rule`',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id, namespase_uid and title columns','CREATE UNIQUE INDEX `UQE_alert_rule_org_id_namespace_uid_title` ON `alert_rule` (`org_id`,`namespace_uid`,`title`);',1,'','2022-01-01 00:00:00'), + ('add dashboard_uid column to alert_rule','alter table `alert_rule` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add panel_id column to alert_rule','alter table `alert_rule` ADD COLUMN `panel_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id, dashboard_uid and panel_id columns','CREATE INDEX `IDX_alert_rule_org_id_dashboard_uid_panel_id` ON `alert_rule` (`org_id`,`dashboard_uid`,`panel_id`);',1,'','2022-01-01 00:00:00'), + ('add rule_group_idx column to alert_rule','alter table `alert_rule` ADD COLUMN `rule_group_idx` INT NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('add is_paused column to alert_rule table','alter table `alert_rule` ADD COLUMN `is_paused` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('fix is_paused column for alert_rule table','',1,'','2022-01-01 00:00:00'), + ('alter table alert_rule alter column rule_group_idx type to bigint','ALTER TABLE alert_rule MODIFY rule_group_idx BIGINT;',1,'','2022-01-01 00:00:00'), + ('create alert_rule_version table','CREATE TABLE IF NOT EXISTS `alert_rule_version` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `rule_org_id` BIGINT(20) NOT NULL\n, `rule_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0\n, `rule_namespace_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `rule_group` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `parent_version` INT NOT NULL\n, `restored_from` INT NOT NULL\n, `version` INT NOT NULL\n, `created` DATETIME NOT NULL\n, `title` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `condition` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `interval_seconds` BIGINT(20) NOT NULL\n, `no_data_state` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'NoData\'\n, `exec_err_state` VARCHAR(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'Alerting\'\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule_version table on rule_org_id, rule_uid and version columns','CREATE UNIQUE INDEX `UQE_alert_rule_version_rule_org_id_rule_uid_version` ON `alert_rule_version` (`rule_org_id`,`rule_uid`,`version`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule_version table on rule_org_id, rule_namespace_uid and rule_group columns','CREATE INDEX `IDX_alert_rule_version_rule_org_id_rule_namespace_uid_rule_group` ON `alert_rule_version` (`rule_org_id`,`rule_namespace_uid`,`rule_group`);',1,'','2022-01-01 00:00:00'), + ('alter alert_rule_version table data column to mediumtext in mysql','ALTER TABLE alert_rule_version MODIFY data MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('add column for to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `for` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column annotations to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `annotations` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add column labels to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `labels` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add rule_group_idx column to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `rule_group_idx` INT NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('add is_paused column to alert_rule_versions table','alter table `alert_rule_version` ADD COLUMN `is_paused` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('fix is_paused column for alert_rule_version table','',1,'','2022-01-01 00:00:00'), + ('alter table alert_rule_version alter column rule_group_idx type to bigint','ALTER TABLE alert_rule_version MODIFY rule_group_idx BIGINT;',1,'','2022-01-01 00:00:00'), + ('create_alert_configuration_table','CREATE TABLE IF NOT EXISTS `alert_configuration` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `alertmanager_configuration` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `configuration_version` VARCHAR(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` INT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add column default in alert_configuration','alter table `alert_configuration` ADD COLUMN `default` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql','ALTER TABLE alert_configuration MODIFY alertmanager_configuration MEDIUMTEXT;',1,'','2022-01-01 00:00:00'), + ('add column org_id in alert_configuration','alter table `alert_configuration` ADD COLUMN `org_id` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add index in alert_configuration table on org_id column','CREATE INDEX `IDX_alert_configuration_org_id` ON `alert_configuration` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add configuration_hash column to alert_configuration','alter table `alert_configuration` ADD COLUMN `configuration_hash` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'not-yet-calculated\' ',1,'','2022-01-01 00:00:00'), + ('create_ngalert_configuration_table','CREATE TABLE IF NOT EXISTS `ngalert_configuration` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `alertmanagers` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created_at` INT NOT NULL\n, `updated_at` INT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index in ngalert_configuration on org_id column','CREATE UNIQUE INDEX `UQE_ngalert_configuration_org_id` ON `ngalert_configuration` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add column send_alerts_to in ngalert_configuration','alter table `ngalert_configuration` ADD COLUMN `send_alerts_to` SMALLINT NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('create provenance_type table','CREATE TABLE IF NOT EXISTS `provenance_type` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `record_key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `record_type` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `provenance` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index to uniquify (record_key, record_type, org_id) columns','CREATE UNIQUE INDEX `UQE_provenance_type_record_type_record_key_org_id` ON `provenance_type` (`record_type`,`record_key`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('create alert_image table','CREATE TABLE IF NOT EXISTS `alert_image` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `token` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `path` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `url` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` DATETIME NOT NULL\n, `expires_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index on token to alert_image table','CREATE UNIQUE INDEX `UQE_alert_image_token` ON `alert_image` (`token`);',1,'','2022-01-01 00:00:00'), + ('support longer URLs in alert_image table','ALTER TABLE alert_image MODIFY url VARCHAR(2048) NOT NULL;',1,'','2022-01-01 00:00:00'), + ('create_alert_configuration_history_table','CREATE TABLE IF NOT EXISTS `alert_configuration_history` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL DEFAULT 0\n, `alertmanager_configuration` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `configuration_hash` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'not-yet-calculated\'\n, `configuration_version` VARCHAR(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` INT NOT NULL\n, `default` TINYINT(1) NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('drop non-unique orgID index on alert_configuration','DROP INDEX `IDX_alert_configuration_org_id` ON `alert_configuration`',1,'','2022-01-01 00:00:00'), + ('drop unique orgID index on alert_configuration if exists','DROP INDEX `UQE_alert_configuration_org_id` ON `alert_configuration`',1,'','2022-01-01 00:00:00'), + ('extract alertmanager configuration history to separate table','code migration',1,'','2022-01-01 00:00:00'), + ('add unique index on orgID to alert_configuration','CREATE UNIQUE INDEX `UQE_alert_configuration_org_id` ON `alert_configuration` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add last_applied column to alert_configuration_history','alter table `alert_configuration_history` ADD COLUMN `last_applied` INT NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add message column to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `message` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('create library_element table v1','CREATE TABLE IF NOT EXISTS `library_element` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `folder_id` BIGINT(20) NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `kind` BIGINT(20) NOT NULL\n, `type` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `model` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `created_by` BIGINT(20) NOT NULL\n, `updated` DATETIME NOT NULL\n, `updated_by` BIGINT(20) NOT NULL\n, `version` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index library_element org_id-folder_id-name-kind','CREATE UNIQUE INDEX `UQE_library_element_org_id_folder_id_name_kind` ON `library_element` (`org_id`,`folder_id`,`name`,`kind`);',1,'','2022-01-01 00:00:00'), + ('create library_element_connection table v1','CREATE TABLE IF NOT EXISTS `library_element_connection` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `element_id` BIGINT(20) NOT NULL\n, `kind` BIGINT(20) NOT NULL\n, `connection_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `created_by` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index library_element_connection element_id-kind-connection_id','CREATE UNIQUE INDEX `UQE_library_element_connection_element_id_kind_connection_id` ON `library_element_connection` (`element_id`,`kind`,`connection_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index library_element org_id_uid','CREATE UNIQUE INDEX `UQE_library_element_org_id_uid` ON `library_element` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('increase max description length to 2048','ALTER TABLE `library_element` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `description` VARCHAR(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('alter library_element model to mediumtext','ALTER TABLE library_element MODIFY model MEDIUMTEXT NOT NULL;',1,'','2022-01-01 00:00:00'), + ('add library_element folder uid','alter table `library_element` ADD COLUMN `folder_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('populate library_element folder_uid','UPDATE library_element\n SET folder_uid = (\n SELECT dashboard.uid\n FROM dashboard\n WHERE library_element.folder_id = dashboard.id AND library_element.org_id = dashboard.org_id\n )',1,'','2022-01-01 00:00:00'), + ('add index library_element org_id-folder_uid-name-kind','CREATE UNIQUE INDEX `UQE_library_element_org_id_folder_uid_name_kind` ON `library_element` (`org_id`,`folder_uid`,`name`,`kind`);',1,'','2022-01-01 00:00:00'), + ('drop unique name in folder index (id)','DROP INDEX `UQE_library_element_org_id_folder_id_name_kind` ON `library_element`',1,'','2022-01-01 00:00:00'), + ('drop unique name in folder index','DROP INDEX `UQE_library_element_org_id_folder_uid_name_kind` ON `library_element`',1,'','2022-01-01 00:00:00'), + ('clone move dashboard alerts to unified alerting','code migration',1,'','2022-01-01 00:00:00'), + ('create data_keys table','CREATE TABLE IF NOT EXISTS `data_keys` (\n`name` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `active` TINYINT(1) NOT NULL\n, `scope` VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `provider` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `encrypted_data` BLOB NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create secrets table','CREATE TABLE IF NOT EXISTS `secrets` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `namespace` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('rename data_keys name column to id','ALTER TABLE `data_keys` CHANGE `name` `id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',1,'','2022-01-01 00:00:00'), + ('add name column into data_keys','alter table `data_keys` ADD COLUMN `name` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('copy data_keys id column values into name','UPDATE data_keys SET name = id',1,'','2022-01-01 00:00:00'), + ('rename data_keys name column to label','ALTER TABLE `data_keys` CHANGE `name` `label` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',1,'','2022-01-01 00:00:00'), + ('rename data_keys id column back to name','ALTER TABLE `data_keys` CHANGE `id` `name` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',1,'','2022-01-01 00:00:00'), + ('create kv_store table v1','CREATE TABLE IF NOT EXISTS `kv_store` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `namespace` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index kv_store.org_id-namespace-key','CREATE UNIQUE INDEX `UQE_kv_store_org_id_namespace_key` ON `kv_store` (`org_id`,`namespace`,`key`);',1,'','2022-01-01 00:00:00'), + ('update dashboard_uid and panel_id from existing annotations','set dashboard_uid and panel_id migration',1,'','2022-01-01 00:00:00'), + ('create permission table','CREATE TABLE IF NOT EXISTS `permission` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `role_id` BIGINT(20) NOT NULL\n, `action` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `scope` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index permission.role_id','CREATE INDEX `IDX_permission_role_id` ON `permission` (`role_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index role_id_action_scope','CREATE UNIQUE INDEX `UQE_permission_role_id_action_scope` ON `permission` (`role_id`,`action`,`scope`);',1,'','2022-01-01 00:00:00'), + ('create role table','CREATE TABLE IF NOT EXISTS `role` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `version` BIGINT(20) NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add column display_name','alter table `role` ADD COLUMN `display_name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add column group_name','alter table `role` ADD COLUMN `group_name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add index role.org_id','CREATE INDEX `IDX_role_org_id` ON `role` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index role_org_id_name','CREATE UNIQUE INDEX `UQE_role_org_id_name` ON `role` (`org_id`,`name`);',1,'','2022-01-01 00:00:00'), + ('add index role_org_id_uid','CREATE UNIQUE INDEX `UQE_role_org_id_uid` ON `role` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('create team role table','CREATE TABLE IF NOT EXISTS `team_role` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `team_id` BIGINT(20) NOT NULL\n, `role_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index team_role.org_id','CREATE INDEX `IDX_team_role_org_id` ON `team_role` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index team_role_org_id_team_id_role_id','CREATE UNIQUE INDEX `UQE_team_role_org_id_team_id_role_id` ON `team_role` (`org_id`,`team_id`,`role_id`);',1,'','2022-01-01 00:00:00'), + ('add index team_role.team_id','CREATE INDEX `IDX_team_role_team_id` ON `team_role` (`team_id`);',1,'','2022-01-01 00:00:00'), + ('create user role table','CREATE TABLE IF NOT EXISTS `user_role` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `role_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index user_role.org_id','CREATE INDEX `IDX_user_role_org_id` ON `user_role` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index user_role_org_id_user_id_role_id','CREATE UNIQUE INDEX `UQE_user_role_org_id_user_id_role_id` ON `user_role` (`org_id`,`user_id`,`role_id`);',1,'','2022-01-01 00:00:00'), + ('add index user_role.user_id','CREATE INDEX `IDX_user_role_user_id` ON `user_role` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('create builtin role table','CREATE TABLE IF NOT EXISTS `builtin_role` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `role` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `role_id` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index builtin_role.role_id','CREATE INDEX `IDX_builtin_role_role_id` ON `builtin_role` (`role_id`);',1,'','2022-01-01 00:00:00'), + ('add index builtin_role.name','CREATE INDEX `IDX_builtin_role_role` ON `builtin_role` (`role`);',1,'','2022-01-01 00:00:00'), + ('Add column org_id to builtin_role table','alter table `builtin_role` ADD COLUMN `org_id` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add index builtin_role.org_id','CREATE INDEX `IDX_builtin_role_org_id` ON `builtin_role` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index builtin_role_org_id_role_id_role','CREATE UNIQUE INDEX `UQE_builtin_role_org_id_role_id_role` ON `builtin_role` (`org_id`,`role_id`,`role`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index role_org_id_uid','DROP INDEX `UQE_role_org_id_uid` ON `role`',1,'','2022-01-01 00:00:00'), + ('add unique index role.uid','CREATE UNIQUE INDEX `UQE_role_uid` ON `role` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create seed assignment table','CREATE TABLE IF NOT EXISTS `seed_assignment` (\n`builtin_role` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `role_name` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index builtin_role_role_name','CREATE UNIQUE INDEX `UQE_seed_assignment_builtin_role_role_name` ON `seed_assignment` (`builtin_role`,`role_name`);',1,'','2022-01-01 00:00:00'), + ('add column hidden to role table','alter table `role` ADD COLUMN `hidden` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('permission kind migration','alter table `permission` ADD COLUMN `kind` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('permission attribute migration','alter table `permission` ADD COLUMN `attribute` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('permission identifier migration','alter table `permission` ADD COLUMN `identifier` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('add permission identifier index','CREATE INDEX `IDX_permission_identifier` ON `permission` (`identifier`);',1,'','2022-01-01 00:00:00'), + ('add permission action scope role_id index','CREATE UNIQUE INDEX `UQE_permission_action_scope_role_id` ON `permission` (`action`,`scope`,`role_id`);',1,'','2022-01-01 00:00:00'), + ('remove permission role_id action scope index','DROP INDEX `UQE_permission_role_id_action_scope` ON `permission`',1,'','2022-01-01 00:00:00'), + ('add group mapping UID column to user_role table','alter table `user_role` ADD COLUMN `group_mapping_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('add user_role org ID, user ID, role ID, group mapping UID index','CREATE UNIQUE INDEX `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` ON `user_role` (`org_id`,`user_id`,`role_id`,`group_mapping_uid`);',1,'','2022-01-01 00:00:00'), + ('remove user_role org ID, user ID, role ID index','DROP INDEX `UQE_user_role_org_id_user_id_role_id` ON `user_role`',1,'','2022-01-01 00:00:00'), + ('add permission role_id action index','CREATE INDEX `IDX_permission_role_id_action` ON `permission` (`role_id`,`action`);',1,'','2022-01-01 00:00:00'), + ('Remove permission role_id index','DROP INDEX `IDX_permission_role_id` ON `permission`',1,'','2022-01-01 00:00:00'), + ('create query_history table v1','CREATE TABLE IF NOT EXISTS `query_history` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `datasource_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_by` INT NOT NULL\n, `created_at` INT NOT NULL\n, `comment` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `queries` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index query_history.org_id-created_by-datasource_uid','CREATE INDEX `IDX_query_history_org_id_created_by_datasource_uid` ON `query_history` (`org_id`,`created_by`,`datasource_uid`);',1,'','2022-01-01 00:00:00'), + ('alter table query_history alter column created_by type to bigint','ALTER TABLE query_history MODIFY created_by BIGINT;',1,'','2022-01-01 00:00:00'), + ('create query_history_details table v1','CREATE TABLE IF NOT EXISTS `query_history_details` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `query_history_item_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `datasource_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('rbac disabled migrator','code migration',1,'','2022-01-01 00:00:00'), + ('teams permissions migration','code migration',1,'','2022-01-01 00:00:00'), + ('dashboard permissions','code migration',1,'','2022-01-01 00:00:00'), + ('dashboard permissions uid scopes','code migration',1,'','2022-01-01 00:00:00'), + ('drop managed folder create actions','code migration',1,'','2022-01-01 00:00:00'), + ('alerting notification permissions','code migration',1,'','2022-01-01 00:00:00'), + ('create query_history_star table v1','CREATE TABLE IF NOT EXISTS `query_history_star` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `query_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `user_id` INT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index query_history.user_id-query_uid','CREATE UNIQUE INDEX `UQE_query_history_star_user_id_query_uid` ON `query_history_star` (`user_id`,`query_uid`);',1,'','2022-01-01 00:00:00'), + ('add column org_id in query_history_star','alter table `query_history_star` ADD COLUMN `org_id` BIGINT(20) NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('alter table query_history_star_mig column user_id type to bigint','ALTER TABLE query_history_star MODIFY user_id BIGINT;',1,'','2022-01-01 00:00:00'), + ('create correlation table v1','CREATE TABLE IF NOT EXISTS `correlation` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `source_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `target_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `label` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, PRIMARY KEY ( `uid`,`source_uid` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index correlations.uid','CREATE INDEX `IDX_correlation_uid` ON `correlation` (`uid`);',1,'','2022-01-01 00:00:00'), + ('add index correlations.source_uid','CREATE INDEX `IDX_correlation_source_uid` ON `correlation` (`source_uid`);',1,'','2022-01-01 00:00:00'), + ('add correlation config column','alter table `correlation` ADD COLUMN `config` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('drop index IDX_correlation_uid - v1','DROP INDEX `IDX_correlation_uid` ON `correlation`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_correlation_source_uid - v1','DROP INDEX `IDX_correlation_source_uid` ON `correlation`',1,'','2022-01-01 00:00:00'), + ('Rename table correlation to correlation_tmp_qwerty - v1','ALTER TABLE `correlation` RENAME TO `correlation_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create correlation v2','CREATE TABLE IF NOT EXISTS `correlation` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL DEFAULT 0\n, `source_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `target_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `label` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `config` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, PRIMARY KEY ( `uid`,`org_id`,`source_uid` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index IDX_correlation_uid - v2','CREATE INDEX `IDX_correlation_uid` ON `correlation` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_correlation_source_uid - v2','CREATE INDEX `IDX_correlation_source_uid` ON `correlation` (`source_uid`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_correlation_org_id - v2','CREATE INDEX `IDX_correlation_org_id` ON `correlation` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('copy correlation v1 to v2','INSERT INTO `correlation` (`target_uid`\n, `label`\n, `description`\n, `config`\n, `uid`\n, `source_uid`)\nSELECT `target_uid`\n, `label`\n, `description`\n, `config`\n, `uid`\n, `source_uid`\nFROM `correlation_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop correlation_tmp_qwerty','DROP TABLE IF EXISTS `correlation_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('add provisioning column','alter table `correlation` ADD COLUMN `provisioned` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add type column','alter table `correlation` ADD COLUMN `type` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'query\' ',1,'','2022-01-01 00:00:00'), + ('create entity_events table','CREATE TABLE IF NOT EXISTS `entity_event` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `entity_id` VARCHAR(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `event_type` VARCHAR(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create dashboard public config v1','CREATE TABLE IF NOT EXISTS `dashboard_public_config` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `time_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `refresh_rate` INT NOT NULL DEFAULT 30\n, `template_variables` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('drop index UQE_dashboard_public_config_uid - v1','DROP INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_dashboard_public_config_org_id_dashboard_uid - v1','DROP INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('Drop old dashboard public config table','DROP TABLE IF EXISTS `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('recreate dashboard public config v1','CREATE TABLE IF NOT EXISTS `dashboard_public_config` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `time_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `refresh_rate` INT NOT NULL DEFAULT 30\n, `template_variables` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_public_config_uid - v1','CREATE UNIQUE INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public_config` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_public_config_org_id_dashboard_uid - v1','CREATE INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public_config` (`org_id`,`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('drop index UQE_dashboard_public_config_uid - v2','DROP INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('drop index IDX_dashboard_public_config_org_id_dashboard_uid - v2','DROP INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('Drop public config table','DROP TABLE IF EXISTS `dashboard_public_config`',1,'','2022-01-01 00:00:00'), + ('Recreate dashboard public config v2','CREATE TABLE IF NOT EXISTS `dashboard_public_config` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `time_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `template_variables` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `access_token` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_by` INT NOT NULL\n, `updated_by` INT NULL\n, `created_at` DATETIME NOT NULL\n, `updated_at` DATETIME NULL\n, `is_enabled` TINYINT(1) NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_public_config_uid - v2','CREATE UNIQUE INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public_config` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create index IDX_dashboard_public_config_org_id_dashboard_uid - v2','CREATE INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public_config` (`org_id`,`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('create index UQE_dashboard_public_config_access_token - v2','CREATE UNIQUE INDEX `UQE_dashboard_public_config_access_token` ON `dashboard_public_config` (`access_token`);',1,'','2022-01-01 00:00:00'), + ('Rename table dashboard_public_config to dashboard_public - v2','ALTER TABLE `dashboard_public_config` RENAME TO `dashboard_public`',1,'','2022-01-01 00:00:00'), + ('add annotations_enabled column','alter table `dashboard_public` ADD COLUMN `annotations_enabled` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add time_selection_enabled column','alter table `dashboard_public` ADD COLUMN `time_selection_enabled` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('delete orphaned public dashboards','DELETE FROM dashboard_public WHERE dashboard_uid NOT IN (SELECT uid FROM dashboard)',1,'','2022-01-01 00:00:00'), + ('add share column','alter table `dashboard_public` ADD COLUMN `share` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'public\' ',1,'','2022-01-01 00:00:00'), + ('backfill empty share column fields with default of public','UPDATE dashboard_public SET share=\'public\' WHERE share=\'\'',1,'','2022-01-01 00:00:00'), + ('create file table','CREATE TABLE IF NOT EXISTS `file` (\n`path` VARCHAR(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `path_hash` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `parent_folder_path_hash` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `contents` BLOB NOT NULL\n, `etag` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `cache_control` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `content_disposition` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated` DATETIME NOT NULL\n, `created` DATETIME NOT NULL\n, `size` BIGINT(20) NOT NULL\n, `mime_type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('file table idx: path natural pk','CREATE UNIQUE INDEX `UQE_file_path_hash` ON `file` (`path_hash`);',1,'','2022-01-01 00:00:00'), + ('file table idx: parent_folder_path_hash fast folder retrieval','CREATE INDEX `IDX_file_parent_folder_path_hash` ON `file` (`parent_folder_path_hash`);',1,'','2022-01-01 00:00:00'), + ('create file_meta table','CREATE TABLE IF NOT EXISTS `file_meta` (\n`path_hash` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` VARCHAR(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('file table idx: path key','CREATE UNIQUE INDEX `UQE_file_meta_path_hash_key` ON `file_meta` (`path_hash`,`key`);',1,'','2022-01-01 00:00:00'), + ('set path collation in file table','',1,'','2022-01-01 00:00:00'), + ('migrate contents column to mediumblob for MySQL','ALTER TABLE file MODIFY contents MEDIUMBLOB;',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key to file table if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE file\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_file_path_hash,\n ADD PRIMARY KEY (path_hash);\n ',1,'','2022-01-01 00:00:00'), + ('drop file_path unique index from file table if it exists (mysql)','ALTER TABLE file DROP INDEX UQE_file_path_hash',1,'','2022-01-01 00:00:00'), + ('add primary key to file table if it doesn\'t exist (mysql)','ALTER TABLE file ADD PRIMARY KEY (path_hash);',1,'','2022-01-01 00:00:00'), + ('add primary key to file table (postgres and sqlite)','',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key to file_meta table if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE file_meta\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_file_meta_path_hash_key,\n ADD PRIMARY KEY (path_hash, `key`);\n ',1,'','2022-01-01 00:00:00'), + ('drop file_path unique index from file_meta table if it exists (mysql)','ALTER TABLE file_meta DROP INDEX UQE_file_meta_path_hash_key',1,'','2022-01-01 00:00:00'), + ('add primary key to file_meta table if it doesn\'t exist (mysql)','ALTER TABLE file_meta ADD PRIMARY KEY (path_hash, `key`);',1,'','2022-01-01 00:00:00'), + ('add primary key to file_meta table (postgres and sqlite)','',1,'','2022-01-01 00:00:00'), + ('managed permissions migration','code migration',1,'','2022-01-01 00:00:00'), + ('managed folder permissions alert actions migration','code migration',1,'','2022-01-01 00:00:00'), + ('RBAC action name migrator','code migration',1,'','2022-01-01 00:00:00'), + ('Add UID column to playlist','alter table `playlist` ADD COLUMN `uid` VARCHAR(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in playlist','UPDATE playlist SET uid=id;',1,'','2022-01-01 00:00:00'), + ('Add index for uid in playlist','CREATE UNIQUE INDEX `UQE_playlist_org_id_uid` ON `playlist` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('update group index for alert rules','code migration',1,'','2022-01-01 00:00:00'), + ('managed folder permissions alert actions repeated migration','code migration',1,'','2022-01-01 00:00:00'), + ('admin only folder/dashboard permission','code migration',1,'','2022-01-01 00:00:00'), + ('add action column to seed_assignment','alter table `seed_assignment` ADD COLUMN `action` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add scope column to seed_assignment','alter table `seed_assignment` ADD COLUMN `scope` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('remove unique index builtin_role_role_name before nullable update','DROP INDEX `UQE_seed_assignment_builtin_role_role_name` ON `seed_assignment`',1,'','2022-01-01 00:00:00'), + ('update seed_assignment role_name column to nullable','ALTER TABLE seed_assignment MODIFY role_name VARCHAR(190) DEFAULT NULL;',1,'','2022-01-01 00:00:00'), + ('add unique index builtin_role_name back','CREATE UNIQUE INDEX `UQE_seed_assignment_builtin_role_role_name` ON `seed_assignment` (`builtin_role`,`role_name`);',1,'','2022-01-01 00:00:00'), + ('add unique index builtin_role_action_scope','CREATE UNIQUE INDEX `UQE_seed_assignment_builtin_role_action_scope` ON `seed_assignment` (`builtin_role`,`action`,`scope`);',1,'','2022-01-01 00:00:00'), + ('add primary key to seed_assigment','code migration',1,'','2022-01-01 00:00:00'), + ('add origin column to seed_assignment','alter table `seed_assignment` ADD COLUMN `origin` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add origin to plugin seed_assignment','code migration',1,'','2022-01-01 00:00:00'), + ('prevent seeding OnCall access','code migration',1,'','2022-01-01 00:00:00'), + ('managed folder permissions alert actions repeated fixed migration','code migration',1,'','2022-01-01 00:00:00'), + ('managed folder permissions library panel actions migration','code migration',1,'','2022-01-01 00:00:00'), + ('migrate external alertmanagers to datsourcse','migrate external alertmanagers to datasource',1,'','2022-01-01 00:00:00'), + ('create folder table','CREATE TABLE IF NOT EXISTS `folder` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `parent_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add index for parent_uid','CREATE INDEX `IDX_folder_parent_uid_org_id` ON `folder` (`parent_uid`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('Add unique index for folder.uid and folder.org_id','CREATE UNIQUE INDEX `UQE_folder_uid_org_id` ON `folder` (`uid`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('Update folder title length','ALTER TABLE `folder` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, MODIFY `title` VARCHAR(189) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ;',1,'','2022-01-01 00:00:00'), + ('Add unique index for folder.title and folder.parent_uid','CREATE UNIQUE INDEX `UQE_folder_title_parent_uid` ON `folder` (`title`,`parent_uid`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index for folder.title and folder.parent_uid','DROP INDEX `UQE_folder_title_parent_uid` ON `folder`',1,'','2022-01-01 00:00:00'), + ('Add unique index for title, parent_uid, and org_id','CREATE UNIQUE INDEX `UQE_folder_title_parent_uid_org_id` ON `folder` (`title`,`parent_uid`,`org_id`);',1,'','2022-01-01 00:00:00'), + ('Sync dashboard and folder table','\n INSERT INTO folder (uid, org_id, title, created, updated)\n SELECT * FROM (SELECT uid, org_id, title, created, updated FROM dashboard WHERE is_folder = 1) AS derived\n ON DUPLICATE KEY UPDATE title=derived.title, updated=derived.updated\n ',1,'','2022-01-01 00:00:00'), + ('Remove ghost folders from the folder table','\n DELETE FROM folder WHERE NOT EXISTS\n (SELECT 1 FROM dashboard WHERE dashboard.uid = folder.uid AND dashboard.org_id = folder.org_id AND dashboard.is_folder = true)\n ',1,'','2022-01-01 00:00:00'), + ('Remove unique index UQE_folder_uid_org_id','DROP INDEX `UQE_folder_uid_org_id` ON `folder`',1,'','2022-01-01 00:00:00'), + ('Add unique index UQE_folder_org_id_uid','CREATE UNIQUE INDEX `UQE_folder_org_id_uid` ON `folder` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index UQE_folder_title_parent_uid_org_id','DROP INDEX `UQE_folder_title_parent_uid_org_id` ON `folder`',1,'','2022-01-01 00:00:00'), + ('Add unique index UQE_folder_org_id_parent_uid_title','CREATE UNIQUE INDEX `UQE_folder_org_id_parent_uid_title` ON `folder` (`org_id`,`parent_uid`,`title`);',1,'','2022-01-01 00:00:00'), + ('Remove index IDX_folder_parent_uid_org_id','DROP INDEX `IDX_folder_parent_uid_org_id` ON `folder`',1,'','2022-01-01 00:00:00'), + ('Remove unique index UQE_folder_org_id_parent_uid_title','DROP INDEX `UQE_folder_org_id_parent_uid_title` ON `folder`',1,'','2022-01-01 00:00:00'), + ('Add index IDX_folder_org_id_parent_uid','CREATE INDEX `IDX_folder_org_id_parent_uid` ON `folder` (`org_id`,`parent_uid`);',1,'','2022-01-01 00:00:00'), + ('create anon_device table','CREATE TABLE IF NOT EXISTS `anon_device` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `client_ip` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` DATETIME NOT NULL\n, `device_id` VARCHAR(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `updated_at` DATETIME NOT NULL\n, `user_agent` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index anon_device.device_id','CREATE UNIQUE INDEX `UQE_anon_device_device_id` ON `anon_device` (`device_id`);',1,'','2022-01-01 00:00:00'), + ('add index anon_device.updated_at','CREATE INDEX `IDX_anon_device_updated_at` ON `anon_device` (`updated_at`);',1,'','2022-01-01 00:00:00'), + ('create signing_key table','CREATE TABLE IF NOT EXISTS `signing_key` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `key_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `private_key` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `added_at` DATETIME NOT NULL\n, `expires_at` DATETIME NULL\n, `alg` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index signing_key.key_id','CREATE UNIQUE INDEX `UQE_signing_key_key_id` ON `signing_key` (`key_id`);',1,'','2022-01-01 00:00:00'), + ('set legacy alert migration status in kvstore','code migration',1,'','2022-01-01 00:00:00'), + ('migrate record of created folders during legacy migration to kvstore','code migration',1,'','2022-01-01 00:00:00'), + ('Add folder_uid for dashboard','alter table `dashboard` ADD COLUMN `folder_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Populate dashboard folder_uid column','code migration',1,'','2022-01-01 00:00:00'), + ('Add unique index for dashboard_org_id_folder_uid_title','code migration',1,'','2022-01-01 00:00:00'), + ('Delete unique index for dashboard_org_id_folder_id_title','DROP INDEX `UQE_dashboard_org_id_folder_id_title` ON `dashboard`',1,'','2022-01-01 00:00:00'), + ('Delete unique index for dashboard_org_id_folder_uid_title','code migration',1,'','2022-01-01 00:00:00'), + ('Add unique index for dashboard_org_id_folder_uid_title_is_folder','CREATE UNIQUE INDEX `UQE_dashboard_org_id_folder_uid_title_is_folder` ON `dashboard` (`org_id`,`folder_uid`,`title`,`is_folder`);',1,'','2022-01-01 00:00:00'), + ('Restore index for dashboard_org_id_folder_id_title','CREATE INDEX `IDX_dashboard_org_id_folder_id_title` ON `dashboard` (`org_id`,`folder_id`,`title`);',1,'','2022-01-01 00:00:00'), + ('Remove unique index for dashboard_org_id_folder_uid_title_is_folder','DROP INDEX `UQE_dashboard_org_id_folder_uid_title_is_folder` ON `dashboard`',1,'','2022-01-01 00:00:00'), + ('create sso_setting table','CREATE TABLE IF NOT EXISTS `sso_setting` (\n`id` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `provider` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n, `is_deleted` TINYINT(1) NOT NULL DEFAULT 0\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('update settings column to MEDIUMTEXT','ALTER TABLE sso_setting MODIFY COLUMN settings MEDIUMTEXT NOT NULL;',1,'','2022-01-01 00:00:00'), + ('copy kvstore migration status to each org','code migration',1,'','2022-01-01 00:00:00'), + ('add back entry for orgid=0 migrated status','code migration',1,'','2022-01-01 00:00:00'), + ('managed dashboard permissions annotation actions migration','code migration',1,'','2022-01-01 00:00:00'), + ('create cloud_migration table v1','CREATE TABLE IF NOT EXISTS `cloud_migration` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `auth_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `stack` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create cloud_migration_run table v1','CREATE TABLE IF NOT EXISTS `cloud_migration_run` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `cloud_migration_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `result` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n, `finished` DATETIME NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add stack_id column','alter table `cloud_migration` ADD COLUMN `stack_id` BIGINT(20) NOT NULL ',1,'','2022-01-01 00:00:00'), + ('add region_slug column','alter table `cloud_migration` ADD COLUMN `region_slug` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ',1,'','2022-01-01 00:00:00'), + ('add cluster_slug column','alter table `cloud_migration` ADD COLUMN `cluster_slug` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ',1,'','2022-01-01 00:00:00'), + ('add migration uid column','alter table `cloud_migration` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values for migration','UPDATE cloud_migration SET uid=concat(\'u\',lpad(id,9,\'0\')) WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add unique index migration_uid','CREATE UNIQUE INDEX `UQE_cloud_migration_uid` ON `cloud_migration` (`uid`);',1,'','2022-01-01 00:00:00'), + ('add migration run uid column','alter table `cloud_migration_run` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values for migration run','UPDATE cloud_migration_run SET uid=concat(\'u\',lpad(id,9,\'0\')) WHERE uid IS NULL;',1,'','2022-01-01 00:00:00'), + ('Add unique index migration_run_uid','CREATE UNIQUE INDEX `UQE_cloud_migration_run_uid` ON `cloud_migration_run` (`uid`);',1,'','2022-01-01 00:00:00'), + ('Rename table cloud_migration to cloud_migration_session_tmp_qwerty - v1','ALTER TABLE `cloud_migration` RENAME TO `cloud_migration_session_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create cloud_migration_session v2','CREATE TABLE IF NOT EXISTS `cloud_migration_session` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `auth_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `slug` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `stack_id` BIGINT(20) NOT NULL\n, `region_slug` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `cluster_slug` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_cloud_migration_session_uid - v2','CREATE UNIQUE INDEX `UQE_cloud_migration_session_uid` ON `cloud_migration_session` (`uid`);',1,'','2022-01-01 00:00:00'), + ('copy cloud_migration_session v1 to v2','INSERT INTO `cloud_migration_session` (`id`\n, `auth_token`\n, `stack_id`\n, `cluster_slug`\n, `updated`\n, `uid`\n, `slug`\n, `region_slug`\n, `created`)\nSELECT `id`\n, `auth_token`\n, `stack_id`\n, `cluster_slug`\n, `updated`\n, `uid`\n, `stack`\n, `region_slug`\n, `created`\nFROM `cloud_migration_session_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop cloud_migration_session_tmp_qwerty','DROP TABLE IF EXISTS `cloud_migration_session_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('Rename table cloud_migration_run to cloud_migration_snapshot_tmp_qwerty - v1','ALTER TABLE `cloud_migration_run` RENAME TO `cloud_migration_snapshot_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('create cloud_migration_snapshot v2','CREATE TABLE IF NOT EXISTS `cloud_migration_snapshot` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `session_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `result` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n, `finished` DATETIME NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create index UQE_cloud_migration_snapshot_uid - v2','CREATE UNIQUE INDEX `UQE_cloud_migration_snapshot_uid` ON `cloud_migration_snapshot` (`uid`);',1,'','2022-01-01 00:00:00'), + ('copy cloud_migration_snapshot v1 to v2','INSERT INTO `cloud_migration_snapshot` (`created`\n, `updated`\n, `finished`\n, `id`\n, `uid`\n, `session_uid`\n, `result`)\nSELECT `created`\n, `updated`\n, `finished`\n, `id`\n, `uid`\n, `cloud_migration_uid`\n, `result`\nFROM `cloud_migration_snapshot_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('drop cloud_migration_snapshot_tmp_qwerty','DROP TABLE IF EXISTS `cloud_migration_snapshot_tmp_qwerty`',1,'','2022-01-01 00:00:00'), + ('add snapshot upload_url column','alter table `cloud_migration_snapshot` ADD COLUMN `upload_url` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add snapshot status column','alter table `cloud_migration_snapshot` ADD COLUMN `status` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL ',1,'','2022-01-01 00:00:00'), + ('add snapshot local_directory column','alter table `cloud_migration_snapshot` ADD COLUMN `local_directory` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add snapshot gms_snapshot_uid column','alter table `cloud_migration_snapshot` ADD COLUMN `gms_snapshot_uid` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add snapshot encryption_key column','alter table `cloud_migration_snapshot` ADD COLUMN `encryption_key` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add snapshot error_string column','alter table `cloud_migration_snapshot` ADD COLUMN `error_string` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('create cloud_migration_resource table v1','CREATE TABLE IF NOT EXISTS `cloud_migration_resource` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource_type` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `resource_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `status` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `error_string` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `snapshot_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('delete cloud_migration_snapshot.result column','ALTER TABLE cloud_migration_snapshot DROP COLUMN result',1,'','2022-01-01 00:00:00'), + ('add cloud_migration_resource.name column','alter table `cloud_migration_resource` ADD COLUMN `name` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add cloud_migration_resource.parent_name column','alter table `cloud_migration_resource` ADD COLUMN `parent_name` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add cloud_migration_session.org_id column','alter table `cloud_migration_session` ADD COLUMN `org_id` BIGINT(20) NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('add cloud_migration_resource.error_code column','alter table `cloud_migration_resource` ADD COLUMN `error_code` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('increase resource_uid column length','ALTER TABLE cloud_migration_resource MODIFY resource_uid NVARCHAR(255);',1,'','2022-01-01 00:00:00'), + ('create cloud_migration_snapshot_partition table v1','CREATE TABLE IF NOT EXISTS `cloud_migration_snapshot_partition` (\n`snapshot_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `partition_number` INT NOT NULL\n, `resource_type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` LONGBLOB NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add cloud_migration_snapshot_partition srp_unique index','CREATE UNIQUE INDEX `UQE_cloud_migration_snapshot_partition_srp_unique` ON `cloud_migration_snapshot_partition` (`snapshot_uid`,`resource_type`,`partition_number`);',1,'','2022-01-01 00:00:00'), + ('add resource_storage_type column to cloud_migration_snapshot table','alter table `cloud_migration_snapshot` ADD COLUMN `resource_storage_type` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add encryption_algo column to cloud_migration_snapshot table','alter table `cloud_migration_snapshot` ADD COLUMN `encryption_algo` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add metadata column to cloud_migration_snapshot table','alter table `cloud_migration_snapshot` ADD COLUMN `metadata` BLOB NULL ',1,'','2022-01-01 00:00:00'), + ('add public_key column to cloud_migration_snapshot table','alter table `cloud_migration_snapshot` ADD COLUMN `public_key` BLOB NULL ',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key with columns snapshot_uid,resource_type,partition_number to table cloud_migration_snapshot_partition if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE cloud_migration_snapshot_partition\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_cloud_migration_snapshot_partition_srp_unique,\n ADD PRIMARY KEY (`snapshot_uid`,`resource_type`,`partition_number`);\n ',1,'','2022-01-01 00:00:00'), + ('drop unique index UQE_cloud_migration_snapshot_partition_srp_unique from cloud_migration_snapshot_partition table if it exists (mysql)','ALTER TABLE cloud_migration_snapshot_partition DROP INDEX UQE_cloud_migration_snapshot_partition_srp_unique',1,'','2022-01-01 00:00:00'), + ('add primary key with columns snapshot_uid,resource_type,partition_number to table cloud_migration_snapshot_partition if it doesn\'t exist (mysql)','ALTER TABLE cloud_migration_snapshot_partition ADD PRIMARY KEY (`snapshot_uid`,`resource_type`,`partition_number`)',1,'','2022-01-01 00:00:00'), + ('add primary key with columns snapshot_uid,resource_type,partition_number to table cloud_migration_snapshot_partition (postgres and sqlite)','',1,'','2022-01-01 00:00:00'), + ('alter kv_store.value to longtext','ALTER TABLE kv_store MODIFY value LONGTEXT NOT NULL;',1,'','2022-01-01 00:00:00'), + ('add notification_settings column to alert_rule table','alter table `alert_rule` ADD COLUMN `notification_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add notification_settings column to alert_rule_version table','alter table `alert_rule_version` ADD COLUMN `notification_settings` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('removing scope from alert.instances:read action migration','code migration',1,'','2022-01-01 00:00:00'), + ('managed folder permissions alerting silences actions migration','code migration',1,'','2022-01-01 00:00:00'), + ('add record column to alert_rule table','alter table `alert_rule` ADD COLUMN `record` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add record column to alert_rule_version table','alter table `alert_rule_version` ADD COLUMN `record` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add resolved_at column to alert_instance table','alter table `alert_instance` ADD COLUMN `resolved_at` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('add last_sent_at column to alert_instance table','alter table `alert_instance` ADD COLUMN `last_sent_at` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add scope to alert.notifications.receivers:read and alert.notifications.receivers.secrets:read','code migration',1,'','2022-01-01 00:00:00'), + ('add metadata column to alert_rule table','alter table `alert_rule` ADD COLUMN `metadata` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add metadata column to alert_rule_version table','alter table `alert_rule_version` ADD COLUMN `metadata` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('delete orphaned service account permissions','code migration',1,'','2022-01-01 00:00:00'), + ('adding action set permissions','code migration',1,'','2022-01-01 00:00:00'), + ('create user_external_session table','CREATE TABLE IF NOT EXISTS `user_external_session` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_auth_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `auth_module` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `access_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `id_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `refresh_token` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `session_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `session_id_hash` CHAR(44) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `name_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `name_id_hash` CHAR(44) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `expires_at` DATETIME NULL\n, `created_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('increase name_id column length to 1024','ALTER TABLE user_external_session MODIFY name_id NVARCHAR(1024);',1,'','2022-01-01 00:00:00'), + ('increase session_id column length to 1024','ALTER TABLE user_external_session MODIFY session_id NVARCHAR(1024);',1,'','2022-01-01 00:00:00'), + ('remove scope from alert.notifications.receivers:create','code migration',1,'','2022-01-01 00:00:00'), + ('add created_by column to alert_rule_version table','alter table `alert_rule_version` ADD COLUMN `created_by` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add updated_by column to alert_rule table','alter table `alert_rule` ADD COLUMN `updated_by` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add alert_rule_state table','CREATE TABLE IF NOT EXISTS `alert_rule_state` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `rule_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data` LONGBLOB NOT NULL\n, `updated_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index to alert_rule_state on org_id and rule_uid columns','CREATE UNIQUE INDEX `UQE_alert_rule_state_org_id_rule_uid` ON `alert_rule_state` (`org_id`,`rule_uid`);',1,'','2022-01-01 00:00:00'), + ('add guid column to alert_rule table','alter table `alert_rule` ADD COLUMN `guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('add rule_guid column to alert_rule_version table','alter table `alert_rule_version` ADD COLUMN `rule_guid` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\' ',1,'','2022-01-01 00:00:00'), + ('cleanup alert_rule_version table','code migration',1,'','2022-01-01 00:00:00'), + ('populate rule guid in alert rule table','code migration',1,'','2022-01-01 00:00:00'), + ('drop index in alert_rule_version table on rule_org_id, rule_uid and version columns','DROP INDEX `UQE_alert_rule_version_rule_org_id_rule_uid_version` ON `alert_rule_version`',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns','CREATE UNIQUE INDEX `UQE_alert_rule_version_rule_org_id_rule_uid_rule_guid_version` ON `alert_rule_version` (`rule_org_id`,`rule_uid`,`rule_guid`,`version`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule_version table on rule_guid and version columns','CREATE UNIQUE INDEX `UQE_alert_rule_version_rule_guid_version` ON `alert_rule_version` (`rule_guid`,`version`);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule table on guid columns','CREATE UNIQUE INDEX `UQE_alert_rule_guid` ON `alert_rule` (`guid`);',1,'','2022-01-01 00:00:00'), + ('add keep_firing_for column to alert_rule','alter table `alert_rule` ADD COLUMN `keep_firing_for` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add keep_firing_for column to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `keep_firing_for` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add missing_series_evals_to_resolve column to alert_rule','alter table `alert_rule` ADD COLUMN `missing_series_evals_to_resolve` SMALLINT NULL ',1,'','2022-01-01 00:00:00'), + ('add missing_series_evals_to_resolve column to alert_rule_version','alter table `alert_rule_version` ADD COLUMN `missing_series_evals_to_resolve` SMALLINT NULL ',1,'','2022-01-01 00:00:00'), + ('remove the datasources:drilldown action','code migration',1,'','2022-01-01 00:00:00'), + ('remove title in folder unique index','DROP INDEX `UQE_alert_rule_org_id_namespace_uid_title` ON `alert_rule`',1,'','2022-01-01 00:00:00'), + ('add fired_at column to alert_instance table','alter table `alert_instance` ADD COLUMN `fired_at` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('ensure rule_group column is case sensitive in returned results','ALTER TABLE alert_rule MODIFY rule_group VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs NOT NULL;',1,'','2022-01-01 00:00:00'), + ('expand created_by in alert_rule_version table','ALTER TABLE `alert_rule_version` MODIFY created_by VARCHAR(190);',1,'','2022-01-01 00:00:00'), + ('expand updated_by in alert_rule table','ALTER TABLE `alert_rule` MODIFY updated_by VARCHAR(190);',1,'','2022-01-01 00:00:00'), + ('add index in alert_rule on org_id, namespace_uid, rule_group and rule_group_idx columns','CREATE INDEX `IDX_alert_rule_org_id_namespace_uid_rule_group_rule_group_idx` ON `alert_rule` (`org_id`,`namespace_uid`,`rule_group`,`rule_group_idx`);',1,'','2022-01-01 00:00:00'), + ('add annotations column to alert_instance table','alter table `alert_instance` ADD COLUMN `annotations` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('ensure namespace_uid column sorts the same way as golang','ALTER TABLE alert_rule MODIFY namespace_uid VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;',1,'','2022-01-01 00:00:00'), + ('ensure rule_group column sorts the same way as golang','ALTER TABLE alert_rule MODIFY rule_group VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;',1,'','2022-01-01 00:00:00'), + ('add \'alert.notifications.receivers.protected:write\' to receiver admins','code migration',1,'','2022-01-01 00:00:00'), + ('create data_source_usage_by_day table','CREATE TABLE IF NOT EXISTS `data_source_usage_by_day` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `data_source_id` BIGINT(20) NOT NULL\n, `day` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `queries` BIGINT(20) NOT NULL\n, `errors` BIGINT(20) NOT NULL\n, `load_duration_ms` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create data_source_usage_by_day(data_source_id) index','CREATE INDEX `IDX_data_source_usage_by_day_data_source_id` ON `data_source_usage_by_day` (`data_source_id`);',1,'','2022-01-01 00:00:00'), + ('create data_source_usage_by_day(data_source_id, day) unique index','CREATE UNIQUE INDEX `UQE_data_source_usage_by_day_data_source_id_day` ON `data_source_usage_by_day` (`data_source_id`,`day`);',1,'','2022-01-01 00:00:00'), + ('create dashboard_usage_by_day table','CREATE TABLE IF NOT EXISTS `dashboard_usage_by_day` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `day` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `views` BIGINT(20) NOT NULL\n, `queries` BIGINT(20) NOT NULL\n, `errors` BIGINT(20) NOT NULL\n, `load_duration` FLOAT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create dashboard_usage_sums table','CREATE TABLE IF NOT EXISTS `dashboard_usage_sums` (\n`dashboard_id` BIGINT(20) PRIMARY KEY NOT NULL\n, `updated` DATETIME NOT NULL\n, `views_last_1_days` BIGINT(20) NOT NULL\n, `views_last_7_days` BIGINT(20) NOT NULL\n, `views_last_30_days` BIGINT(20) NOT NULL\n, `views_total` BIGINT(20) NOT NULL\n, `queries_last_1_days` BIGINT(20) NOT NULL\n, `queries_last_7_days` BIGINT(20) NOT NULL\n, `queries_last_30_days` BIGINT(20) NOT NULL\n, `queries_total` BIGINT(20) NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create dashboard_usage_by_day(dashboard_id) index','CREATE INDEX `IDX_dashboard_usage_by_day_dashboard_id` ON `dashboard_usage_by_day` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('create dashboard_usage_by_day(dashboard_id, day) index','CREATE UNIQUE INDEX `UQE_dashboard_usage_by_day_dashboard_id_day` ON `dashboard_usage_by_day` (`dashboard_id`,`day`);',1,'','2022-01-01 00:00:00'), + ('add column errors_last_1_days to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `errors_last_1_days` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column errors_last_7_days to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `errors_last_7_days` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column errors_last_30_days to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `errors_last_30_days` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column errors_total to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `errors_total` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('create dashboard_public_usage_by_day table','CREATE TABLE IF NOT EXISTS `dashboard_public_usage_by_day` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `public_dashboard_uid` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `day` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `views` BIGINT(20) NOT NULL\n, `queries` BIGINT(20) NOT NULL\n, `errors` BIGINT(20) NOT NULL\n, `load_duration` FLOAT NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add column cached_queries to dashboard_usage_by_day table','alter table `dashboard_usage_by_day` ADD COLUMN `cached_queries` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column cached_queries to dashboard_public_usage_by_day table','alter table `dashboard_public_usage_by_day` ADD COLUMN `cached_queries` BIGINT(20) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('add column dashboard_uid to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add column org_id to dashboard_usage_sums','alter table `dashboard_usage_sums` ADD COLUMN `org_id` BIGINT(20) NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('add column dashboard_uid to dashboard_usage_by_day','alter table `dashboard_usage_by_day` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add column org_id to dashboard_usage_by_day','alter table `dashboard_usage_by_day` ADD COLUMN `org_id` BIGINT(20) NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('create dashboard_usage_by_day(dashboard_uid, org_id, day) unique index','CREATE UNIQUE INDEX `UQE_dashboard_usage_by_day_dashboard_uid_org_id_day` ON `dashboard_usage_by_day` (`dashboard_uid`,`org_id`,`day`);',1,'','2022-01-01 00:00:00'), + ('Add missing dashboard_uid and org_id to dashboard_usage_by_day and dashboard_usage_sums','code migration',1,'','2022-01-01 00:00:00'), + ('Add dashboard_usage_sums(org_id, dashboard_uid) index','CREATE UNIQUE INDEX `UQE_dashboard_usage_sums_org_id_dashboard_uid` ON `dashboard_usage_sums` (`org_id`,`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('create user_dashboard_views table','CREATE TABLE IF NOT EXISTS `user_dashboard_views` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `viewed` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index user_dashboard_views.user_id','CREATE INDEX `IDX_user_dashboard_views_user_id` ON `user_dashboard_views` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('add index user_dashboard_views.dashboard_id','CREATE INDEX `IDX_user_dashboard_views_dashboard_id` ON `user_dashboard_views` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index user_dashboard_views_user_id_dashboard_id','CREATE UNIQUE INDEX `UQE_user_dashboard_views_user_id_dashboard_id` ON `user_dashboard_views` (`user_id`,`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('add org_id column to user_dashboard_views','alter table `user_dashboard_views` ADD COLUMN `org_id` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('add dashboard_uid column to user_dashboard_views','alter table `user_dashboard_views` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('add unique index user_dashboard_views_org_id_dashboard_uid','CREATE INDEX `IDX_user_dashboard_views_org_id_dashboard_uid` ON `user_dashboard_views` (`org_id`,`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('add unique index user_dashboard_views_org_id_user_id_dashboard_uid','CREATE UNIQUE INDEX `UQE_user_dashboard_views_user_id_org_id_dashboard_uid` ON `user_dashboard_views` (`user_id`,`org_id`,`dashboard_uid`);',1,'','2022-01-01 00:00:00'), + ('populate user_dashboard_views.dashboard_uid and user_dashboard_views.org_id from dashboard table','views migration',1,'','2022-01-01 00:00:00'), + ('delete views where dashboard does not exist','DELETE FROM user_dashboard_views WHERE (dashboard_uid IS NULL OR org_id IS NULL) AND NOT EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = user_dashboard_views.dashboard_id)',1,'','2022-01-01 00:00:00'), + ('create user_stats table','CREATE TABLE IF NOT EXISTS `user_stats` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `billing_role` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index user_stats(user_id)','CREATE UNIQUE INDEX `UQE_user_stats_user_id` ON `user_stats` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('create data_source_cache table','CREATE TABLE IF NOT EXISTS `data_source_cache` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `data_source_id` BIGINT(20) NOT NULL\n, `enabled` TINYINT(1) NOT NULL\n, `ttl_ms` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index data_source_cache.data_source_id','CREATE INDEX `IDX_data_source_cache_data_source_id` ON `data_source_cache` (`data_source_id`);',1,'','2022-01-01 00:00:00'), + ('add use_default_ttl column','alter table `data_source_cache` ADD COLUMN `use_default_ttl` TINYINT(1) NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('add data_source_cache.data_source_uid column','alter table `data_source_cache` ADD COLUMN `data_source_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('remove abandoned data_source_cache records','DELETE FROM data_source_cache WHERE NOT EXISTS (SELECT 1 FROM data_source WHERE id = data_source_cache.data_source_id);',1,'','2022-01-01 00:00:00'), + ('update data_source_cache.data_source_uid value','UPDATE data_source_cache JOIN data_source ON data_source_cache.data_source_id = data_source.id SET data_source_cache.data_source_uid = data_source.uid;',1,'','2022-01-01 00:00:00'), + ('add index data_source_cache.data_source_uid','CREATE INDEX `IDX_data_source_cache_data_source_uid` ON `data_source_cache` (`data_source_uid`);',1,'','2022-01-01 00:00:00'), + ('add data_source_cache.ttl_resources_ms column','alter table `data_source_cache` ADD COLUMN `ttl_resources_ms` BIGINT(20) NOT NULL DEFAULT 300000 ',1,'','2022-01-01 00:00:00'), + ('update data_source_cache.ttl_resources_ms to have the same value as ttl_ms','UPDATE data_source_cache SET ttl_resources_ms = ttl_ms;',1,'','2022-01-01 00:00:00'), + ('create data_source_acl table','CREATE TABLE IF NOT EXISTS `data_source_acl` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `data_source_id` BIGINT(20) NOT NULL\n, `team_id` BIGINT(20) NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `permission` SMALLINT NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index data_source_acl.data_source_id','CREATE INDEX `IDX_data_source_acl_data_source_id` ON `data_source_acl` (`data_source_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index datasource_acl.unique','CREATE UNIQUE INDEX `UQE_data_source_acl_data_source_id_team_id_user_id` ON `data_source_acl` (`data_source_id`,`team_id`,`user_id`);',1,'','2022-01-01 00:00:00'), + ('create license_token table','CREATE TABLE IF NOT EXISTS `license_token` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `token` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('drop recorded_queries table v14','DROP TABLE IF EXISTS `recorded_queries`',1,'','2022-01-01 00:00:00'), + ('drop recording_rules table v14','DROP TABLE IF EXISTS `recording_rules`',1,'','2022-01-01 00:00:00'), + ('create recording_rules table v14','CREATE TABLE IF NOT EXISTS `recording_rules` (\n`id` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `target_ref_id` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `interval` BIGINT(20) NOT NULL\n, `range` BIGINT(20) NOT NULL\n, `active` TINYINT(1) NOT NULL DEFAULT 0\n, `count` TINYINT(1) NOT NULL DEFAULT 0\n, `queries` BLOB NOT NULL\n, `created_at` DATETIME NOT NULL\n, PRIMARY KEY ( `id`,`target_ref_id` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create remote_write_targets table v1','CREATE TABLE IF NOT EXISTS `remote_write_targets` (\n`id` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data_source_uid` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `write_path` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` INT NOT NULL\n, PRIMARY KEY ( `id`,`data_source_uid`,`write_path` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add prom_name to recording_rules table','alter table `recording_rules` ADD COLUMN `prom_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('ensure remote_write_targets table','CREATE TABLE IF NOT EXISTS `remote_write_targets` (\n`id` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `data_source_uid` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `write_path` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `org_id` INT NOT NULL\n, PRIMARY KEY ( `id`,`data_source_uid`,`write_path` )) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create report config table v1','CREATE TABLE IF NOT EXISTS `report` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `dashboard_id` BIGINT(20) NOT NULL\n, `name` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `recipients` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `reply_to` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `message` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `schedule_frequency` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `schedule_day` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `schedule_hour` BIGINT(20) NOT NULL\n, `schedule_minute` BIGINT(20) NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add index report.user_id','CREATE INDEX `IDX_report_user_id` ON `report` (`user_id`);',1,'','2022-01-01 00:00:00'), + ('add index to dashboard_id','CREATE INDEX `IDX_report_dashboard_id` ON `report` (`dashboard_id`);',1,'','2022-01-01 00:00:00'), + ('add index to org_id','CREATE INDEX `IDX_report_org_id` ON `report` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('Add timezone to the report','alter table `report` ADD COLUMN `schedule_timezone` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'Europe/Stockholm\' ',1,'','2022-01-01 00:00:00'), + ('Add time_from to the report','alter table `report` ADD COLUMN `time_from` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add time_to to the report','alter table `report` ADD COLUMN `time_to` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add PDF landscape option to the report','alter table `report` ADD COLUMN `pdf_landscape` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('Add monthly day scheduling option to the report','alter table `report` ADD COLUMN `schedule_day_of_month` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add PDF layout option to the report','alter table `report` ADD COLUMN `pdf_layout` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add PDF orientation option to the report','alter table `report` ADD COLUMN `pdf_orientation` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update report pdf_orientation from pdf_landscape','UPDATE report SET pdf_orientation = \'landscape\' WHERE pdf_landscape = 1',1,'','2022-01-01 00:00:00'), + ('create report settings table','CREATE TABLE IF NOT EXISTS `report_settings` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `user_id` BIGINT(20) NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `branding_report_logo_url` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `branding_email_logo_url` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `branding_email_footer_link` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `branding_email_footer_text` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `branding_email_footer_mode` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add dashboard_uid field to the report','alter table `report` ADD COLUMN `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add template_vars field to the report','alter table `report` ADD COLUMN `template_vars` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add option to include dashboard url in the report','alter table `report` ADD COLUMN `enable_dashboard_url` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('Add state field to the report','alter table `report` ADD COLUMN `state` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add option to add CSV files to the report','alter table `report` ADD COLUMN `enable_csv` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('Add scheduling start date','alter table `report` ADD COLUMN `schedule_start` INT NULL ',1,'','2022-01-01 00:00:00'), + ('Add missing schedule_start date for old reports','code migration',1,'','2022-01-01 00:00:00'), + ('Add scheduling end date','alter table `report` ADD COLUMN `schedule_end` INT NULL ',1,'','2022-01-01 00:00:00'), + ('Add schedulinng custom interval frequency','alter table `report` ADD COLUMN `schedule_interval_frequency` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add scheduling custom interval amount','alter table `report` ADD COLUMN `schedule_interval_amount` BIGINT(20) NULL ',1,'','2022-01-01 00:00:00'), + ('Add workdays only flag to report','alter table `report` ADD COLUMN `schedule_workdays_only` TINYINT(1) NULL ',1,'','2022-01-01 00:00:00'), + ('create report dashboards table','CREATE TABLE IF NOT EXISTS `report_dashboards` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `report_id` BIGINT(20) NOT NULL\n, `dashboard_uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'\'\n, `report_variables` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `time_to` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n, `time_from` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('Add index report_dashboards.report_id','CREATE INDEX `IDX_report_dashboards_report_id` ON `report_dashboards` (`report_id`);',1,'','2022-01-01 00:00:00'), + ('Migrate report fields into report_dashboards','INSERT INTO report_dashboards (report_id, dashboard_uid, report_variables, time_to, time_from) SELECT id, CASE WHEN dashboard_uid IS NULL THEN \'\' ELSE dashboard_uid END, template_vars, time_to, time_from FROM report',1,'','2022-01-01 00:00:00'), + ('Add formats option to the report','alter table `report` ADD COLUMN `formats` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'[\"pdf\"]\' ',1,'','2022-01-01 00:00:00'), + ('Migrate reports with csv enabled','UPDATE report SET formats=\'[\"pdf\",\"csv\"]\' WHERE enable_csv IS TRUE;',1,'','2022-01-01 00:00:00'), + ('Migrate ancient reports','code migration',1,'','2022-01-01 00:00:00'), + ('Add created column in report_dashboards','alter table `report_dashboards` ADD COLUMN `created` DATETIME NULL ',1,'','2022-01-01 00:00:00'), + ('Add scale_factor to the report','alter table `report` ADD COLUMN `scale_factor` TINYINT NOT NULL DEFAULT 2 ',1,'','2022-01-01 00:00:00'), + ('Alter scale_factor from TINYINT to SMALLINT','ALTER TABLE report MODIFY scale_factor SMALLINT NOT NULL DEFAULT 2',1,'','2022-01-01 00:00:00'), + ('Add uid column to report','alter table `report` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Add unique index reports_org_id_uid','CREATE UNIQUE INDEX `UQE_report_org_id_uid` ON `report` (`org_id`,`uid`);',1,'','2022-01-01 00:00:00'), + ('Add pdf show template variable values to the report','alter table `report` ADD COLUMN `pdf_show_template_variables` TINYINT(1) NOT NULL DEFAULT 0 ',1,'','2022-01-01 00:00:00'), + ('Add pdf combine in one file','alter table `report` ADD COLUMN `pdf_combine_one_file` TINYINT(1) NOT NULL DEFAULT 1 ',1,'','2022-01-01 00:00:00'), + ('Add pdf theme to report settings table','alter table `report_settings` ADD COLUMN `pdf_theme` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'light\' ',1,'','2022-01-01 00:00:00'), + ('Add email subject to the report','alter table `report` ADD COLUMN `subject` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Populate email subject with report name','UPDATE report SET subject = name WHERE subject IS NULL',1,'','2022-01-01 00:00:00'), + ('Add embedded image theme to report settings table','alter table `report_settings` ADD COLUMN `embedded_image_theme` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'dark\' ',1,'','2022-01-01 00:00:00'), + ('create team group table','CREATE TABLE IF NOT EXISTS `team_group` (\n`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL\n, `org_id` BIGINT(20) NOT NULL\n, `team_id` BIGINT(20) NOT NULL\n, `group_id` VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created` DATETIME NOT NULL\n, `updated` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add index team_group.org_id','CREATE INDEX `IDX_team_group_org_id` ON `team_group` (`org_id`);',1,'','2022-01-01 00:00:00'), + ('add unique index team_group.org_id_team_id_group_id','CREATE UNIQUE INDEX `UQE_team_group_org_id_team_id_group_id` ON `team_group` (`org_id`,`team_id`,`group_id`);',1,'','2022-01-01 00:00:00'), + ('add index team_group.group_id','CREATE INDEX `IDX_team_group_group_id` ON `team_group` (`group_id`);',1,'','2022-01-01 00:00:00'), + ('Add column uid in team_group','alter table `team_group` ADD COLUMN `uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('Update uid column values in team_group','UPDATE team_group SET uid=concat(\'tg\',lpad(id,9,\'0\')) WHERE uid IS NULL OR uid = \'\';',1,'','2022-01-01 00:00:00'), + ('Add unique index team_group_uid','CREATE UNIQUE INDEX `UQE_team_group_uid` ON `team_group` (`uid`);',1,'','2022-01-01 00:00:00'), + ('create settings table','CREATE TABLE IF NOT EXISTS `setting` (\n`section` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `key` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add unique index settings.section_key','CREATE UNIQUE INDEX `UQE_setting_section_key` ON `setting` (`section`,`key`);',1,'','2022-01-01 00:00:00'), + ('add setting.encrypted_value','alter table `setting` ADD COLUMN `encrypted_value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL ',1,'','2022-01-01 00:00:00'), + ('drop my_row_id and add primary key to setting table if my_row_id exists (auto-generated mysql column)','\n ALTER TABLE setting\n DROP PRIMARY KEY,\n DROP COLUMN my_row_id,\n DROP INDEX UQE_setting_section_key,\n ADD PRIMARY KEY (section, `key`);\n ',1,'','2022-01-01 00:00:00'), + ('drop setting_section unique index from setting table if it exists (mysql)','ALTER TABLE setting DROP INDEX UQE_setting_section_key',1,'','2022-01-01 00:00:00'), + ('add primary key to setting table if it doesn\'t exist (mysql)','ALTER TABLE setting ADD PRIMARY KEY (section, `key`);',1,'','2022-01-01 00:00:00'), + ('add primary key to setting table (postgres and sqlite)','',1,'','2022-01-01 00:00:00'), + ('migrate role names','code migration',1,'','2022-01-01 00:00:00'), + ('rename orgs roles','code migration',1,'','2022-01-01 00:00:00'), + ('remove duplicated org role','code migration',1,'','2022-01-01 00:00:00'), + ('migrate alerting role names','code migration',1,'','2022-01-01 00:00:00'), + ('data source permissions','code migration',1,'','2022-01-01 00:00:00'), + ('data source uid permissions','code migration',1,'','2022-01-01 00:00:00'), + ('rename permissions:delegate scope','UPDATE permission SET scope = \'permissions:type:delegate\' WHERE scope = \'permissions:delegate\'',1,'','2022-01-01 00:00:00'), + ('remove invalid managed permissions','code migration',1,'','2022-01-01 00:00:00'), + ('builtin role migration','code migration',1,'','2022-01-01 00:00:00'), + ('seed permissions migration','code migration',1,'','2022-01-01 00:00:00'), + ('managed permissions migration enterprise','code migration',1,'','2022-01-01 00:00:00'), + ('create table dashboard_public_email_share','CREATE TABLE IF NOT EXISTS `dashboard_public_email_share` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `public_dashboard_uid` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `recipient` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `type` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT \'email\'\n, `created_at` DATETIME NOT NULL\n, `updated_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table dashboard_public_magic_link','CREATE TABLE IF NOT EXISTS `dashboard_public_magic_link` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `token_uuid` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `public_dashboard_uid` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` DATETIME NOT NULL\n, `updated_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('create table dashboard_public_session','CREATE TABLE IF NOT EXISTS `dashboard_public_session` (\n`uid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY NOT NULL\n, `cookie_uuid` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `public_dashboard_uid` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL\n, `created_at` DATETIME NOT NULL\n, `updated_at` DATETIME NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;',1,'','2022-01-01 00:00:00'), + ('add last_seen_at column','alter table `dashboard_public_session` ADD COLUMN `last_seen_at` DATETIME NULL ',1,'','2022-01-01 00:00:00'); +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/pkg/services/sqlstore/migrator/testdata/sqlite_file_migration_statement.sql b/pkg/services/sqlstore/migrator/testdata/sqlite_file_migration_statement.sql new file mode 100644 index 00000000000..2f37839d842 --- /dev/null +++ b/pkg/services/sqlstore/migrator/testdata/sqlite_file_migration_statement.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS `file_new` ( + `path` TEXT NOT NULL + , `path_hash` TEXT NOT NULL + , `parent_folder_path_hash` TEXT NOT NULL + , `contents` BLOB NOT NULL + , `etag` TEXT NOT NULL + , PRIMARY KEY ( `path_hash`,`etag` )); + +INSERT INTO `file_new` (`path` + , `path_hash` + , `parent_folder_path_hash` + , `contents` + , `etag`) +SELECT `path` + , `path_hash` + , `parent_folder_path_hash` + , `contents` + , `etag` +FROM `file`; + +DROP TABLE IF EXISTS `file`; +ALTER TABLE `file_new` RENAME TO `file`; +CREATE INDEX `IDX_file_parent_folder_path_hash` ON `file` (`parent_folder_path_hash`); diff --git a/pkg/services/sqlstore/migrator/testing.go b/pkg/services/sqlstore/migrator/testing.go new file mode 100644 index 00000000000..cfe7849c79e --- /dev/null +++ b/pkg/services/sqlstore/migrator/testing.go @@ -0,0 +1,51 @@ +package migrator + +import ( + "fmt" + "strings" +) + +type ExpectedMigration struct { + Id string + SQL string +} + +// CheckExpectedMigrations verifies that given migrations exist in migrator after running addMigrations function, +// that they are in the same order and have expected SQL. +func CheckExpectedMigrations(dialectName string, expected []ExpectedMigration, addMigrations func(migrator *Migrator)) error { + d := NewDialect(dialectName) + mg := newMigrator(nil, nil, "", d) + addMigrations(mg) + + migrations := mg.migrations + migrationNames := make([]string, 0, len(migrations)) + for _, m := range expected { + for ; len(migrations) > 0 && migrations[0].Id() != m.Id; migrations = migrations[1:] { + migrationNames = append(migrationNames, migrations[0].Id()) + } + + if len(migrations) == 0 { + return fmt.Errorf("migration `%s` not found, existing migrations:\n%s", m.Id, strings.Join(migrationNames, "\n")) + } + + sql := migrations[0].SQL(d) + if normalizeLines(m.SQL) != normalizeLines(sql) { + return fmt.Errorf("migration `%s` has wrong SQL:\nexpected:\n%s\nactual:\n%s", m.Id, m.SQL, sql) + } + } + return nil +} + +func normalizeLines(sql string) string { + lines := strings.Split(sql, "\n") + result := strings.Builder{} + for _, l := range lines { + l := strings.TrimSpace(l) + if l == "" { + continue + } + result.WriteString(l) + result.WriteString("\n") + } + return result.String() +} diff --git a/pkg/services/updatemanager/plugins.go b/pkg/services/updatemanager/plugins.go index 79270f8a1ee..7ee11b261f9 100644 --- a/pkg/services/updatemanager/plugins.go +++ b/pkg/services/updatemanager/plugins.go @@ -13,6 +13,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/open-feature/go-sdk/openfeature" "go.opentelemetry.io/otel/codes" "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" @@ -96,11 +97,7 @@ func (s *PluginsService) IsDisabled() bool { } func (s *PluginsService) Run(ctx context.Context) error { - s.instrumentedCheckForUpdates(ctx) - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { - s.updateAll(ctx) - } + s.checkAndUpdate(ctx) ticker := time.NewTicker(time.Minute * 10) run := true @@ -108,11 +105,7 @@ func (s *PluginsService) Run(ctx context.Context) error { for run { select { case <-ticker.C: - s.instrumentedCheckForUpdates(ctx) - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { - s.updateAll(ctx) - } + s.checkAndUpdate(ctx) case <-ctx.Done(): run = false } @@ -140,6 +133,14 @@ func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string return "", false } +// checkAndUpdate checks for updates and applies them if auto-update is enabled. +func (s *PluginsService) checkAndUpdate(ctx context.Context) { + s.instrumentedCheckForUpdates(ctx) + if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { + s.updateAll(ctx) + } +} + func (s *PluginsService) instrumentedCheckForUpdates(ctx context.Context) { start := time.Now() ctx, span := s.tracer.Start(ctx, "updatechecker.PluginsService.checkForUpdates") @@ -226,8 +227,7 @@ func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugi return false } - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { + if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, s.updateStrategy == setting.PluginUpdateStrategyMinor) } diff --git a/pkg/services/updatemanager/plugins_test.go b/pkg/services/updatemanager/plugins_test.go index 0455963f168..75834c44c7d 100644 --- a/pkg/services/updatemanager/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -6,8 +6,10 @@ import ( "net/http" "net/url" "strings" + "sync" "testing" + "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -19,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" + "github.com/grafana/grafana/pkg/setting" ) type mockPluginPreinstall struct { @@ -240,6 +243,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { require.Empty(t, svc.availableUpdates["test-core-panel"]) }) } + func TestPluginUpdateChecker_updateAll(t *testing.T) { t.Run("update is available", func(t *testing.T) { pluginsFakeStore := map[string]string{} @@ -300,3 +304,88 @@ func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { return resp, nil } + +func TestPluginsService_PluginsAutoUpdateFlag(t *testing.T) { + updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck") + + tests := []struct { + name string + flagEnabled bool + expectUpdate bool + }{ + { + name: "pluginsAutoUpdate enabled calls updateAll", + flagEnabled: true, + expectUpdate: true, + }, + { + name: "pluginsAutoUpdate disabled does not call updateAll", + flagEnabled: false, + expectUpdate: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupOpenFeatureProvider(t, tt.flagEnabled) + + updateCallCount := 0 + + availableUpdates := map[string]availableUpdate{ + "test-plugin": { + localVersion: "0.9.0", + availableVersion: "1.0.0", + }, + } + + svc := &PluginsService{ + availableUpdates: availableUpdates, + httpClient: &fakeHTTPClient{ + fakeResp: `[]`, + }, + log: log.NewNopLogger(), + tracer: tracing.InitializeTracerForTest(), + updateCheckURL: updateCheckURL, + updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}), + pluginStore: &pluginstore.FakePluginStore{PluginList: []pluginstore.Plugin{}}, + pluginInstaller: &pluginfakes.FakePluginInstaller{ + AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error { + updateCallCount++ + return nil + }, + }, + grafanaVersion: "10.0.0", + } + + ctx := context.Background() + // Test the synchronous initialization work directly, without the long-running ticker loop + svc.checkAndUpdate(ctx) + + if tt.expectUpdate { + require.Equal(t, updateCallCount, 1, "updateAll should be called when flag is enabled") + } else { + require.Equal(t, 0, updateCallCount, "updateAll should not be called when flag is disabled") + } + }) + } +} + +var openfeatureTestMutex sync.Mutex + +func setupOpenFeatureProvider(t *testing.T, flagValue bool) { + t.Helper() + openfeatureTestMutex.Lock() + + err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{ + ProviderType: setting.StaticProviderType, + StaticFlags: map[string]bool{ + featuremgmt.FlagPluginsAutoUpdate: flagValue, + }, + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + openfeatureTestMutex.Unlock() + }) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f6c0b3d3f19..995a01ad825 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -247,11 +247,12 @@ type Cfg struct { MetricsGrafanaEnvironmentInfo map[string]string // Dashboards - DashboardVersionsToKeep int - MinRefreshInterval string - DefaultHomeDashboardPath string - DashboardPerformanceMetrics []string - PanelSeriesLimit int + DashboardVersionsToKeep int + MinRefreshInterval string + DefaultHomeDashboardPath string + DashboardPerformanceMetrics []string + PanelSeriesLimit int + DashboardSchemaMigrationCacheTTL time.Duration // Auth LoginCookieName string @@ -413,6 +414,7 @@ type Cfg struct { RudderstackDataPlaneURL string RudderstackWriteKey string RudderstackSDKURL string + RudderstackV3SDKURL string RudderstackConfigURL string RudderstackIntegrationsURL string IntercomSecret string @@ -617,6 +619,7 @@ type Cfg struct { EnableSearch bool OverridesFilePath string OverridesReloadInterval time.Duration + EnableSQLKVBackend bool // Secrets Management SecretsManagement SecretsManagerSettings @@ -630,6 +633,9 @@ type UnifiedStorageConfig struct { DataSyncerInterval time.Duration // DataSyncerRecordsLimit defines how many records will be processed at max during a sync invocation. DataSyncerRecordsLimit int + // EnableMigration indicates whether migration is enabled for the resource. + // If not set, will use the default from MigratedUnifiedResources. + EnableMigration bool } type InstallPlugin struct { @@ -1237,6 +1243,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) cfg.PanelSeriesLimit = dashboards.Key("panel_series_limit").MustInt(0) + cfg.DashboardSchemaMigrationCacheTTL = dashboards.Key("schema_migration_cache_ttl").MustDuration(time.Minute) if err := readUserSettings(iniFile, cfg); err != nil { return err @@ -1275,6 +1282,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.RudderstackWriteKey = analytics.Key("rudderstack_write_key").String() cfg.RudderstackDataPlaneURL = analytics.Key("rudderstack_data_plane_url").String() cfg.RudderstackSDKURL = analytics.Key("rudderstack_sdk_url").String() + cfg.RudderstackV3SDKURL = analytics.Key("rudderstack_v3_sdk_url").String() cfg.RudderstackConfigURL = analytics.Key("rudderstack_config_url").String() cfg.RudderstackIntegrationsURL = analytics.Key("rudderstack_integrations_url").String() cfg.IntercomSecret = analytics.Key("intercom_secret").String() @@ -1330,7 +1338,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) shortLinks := iniFile.Section("short_links") - cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(7) + cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(-1) if cfg.ShortLinkExpiration > 365 { cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days") @@ -2159,7 +2167,7 @@ func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error { } cfg.ProvisioningAllowedTargets = iniFile.Section("provisioning").Key("allowed_targets").Strings("|") if len(cfg.ProvisioningAllowedTargets) == 0 { - cfg.ProvisioningAllowedTargets = []string{"instance", "folder"} + cfg.ProvisioningAllowedTargets = []string{"folder"} } cfg.ProvisioningAllowImageRendering = iniFile.Section("provisioning").Key("allow_image_rendering").MustBool(true) cfg.ProvisioningMinSyncInterval = iniFile.Section("provisioning").Key("min_sync_interval").MustDuration(10 * time.Second) diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index a6842a276a0..5a83c3b1f5a 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -102,7 +102,7 @@ func (cfg *Cfg) processPreinstallPlugins(rawInstallPlugins []string, preinstallP if len(parts) > 1 { version = parts[1] if len(parts) > 2 { - url = parts[2] + url = strings.Join(parts[2:], "@") } } diff --git a/pkg/setting/setting_plugins_test.go b/pkg/setting/setting_plugins_test.go index 9b0df2aa583..4c29e5d3fdc 100644 --- a/pkg/setting/setting_plugins_test.go +++ b/pkg/setting/setting_plugins_test.go @@ -210,6 +210,11 @@ func Test_readPluginSettings(t *testing.T) { rawInput: "plugin1@@https://example.com/plugin1.tar.gz", expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: "https://example.com/plugin1.tar.gz"}), }, + { + name: "should parse a plugin with credentials in the URL", + rawInput: "plugin1@@https://username:password@example.com/plugin1.tar.gz", + expected: append(defaultPreinstallPluginsList, InstallPlugin{ID: "plugin1", Version: "", URL: "https://username:password@example.com/plugin1.tar.gz"}), + }, { name: "when preinstall_async is false, should add all plugins to preinstall_sync", rawInput: "plugin1", diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 7a365aec624..6abaef8bc2e 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -153,6 +153,11 @@ type UnifiedAlertingSettings struct { // DeletedRuleRetention defines the maximum duration to retain deleted alerting rules before permanent removal. DeletedRuleRetention time.Duration + + // AlertmanagerMaxTemplateOutputSize specifies the maximum allowed size for rendered template output in bytes. + AlertmanagerMaxTemplateOutputSize int64 + + BacktestingMaxEvaluations int } type RecordingRuleSettings struct { @@ -187,6 +192,8 @@ type UnifiedAlertingReservedLabelSettings struct { type UnifiedAlertingPrometheusConversionSettings struct { // RuleQueryOffset defines a time offset to apply to rule queries during conversion from Prometheus to Grafana format RuleQueryOffset time.Duration + // DefaultDatasourceUID is the default datasource UID to use when converting Prometheus rules if not specified via header + DefaultDatasourceUID string } type UnifiedAlertingLokiSettings struct { @@ -533,7 +540,8 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { prometheusConversion := iniFile.Section("unified_alerting.prometheus_conversion") uaCfg.PrometheusConversion = UnifiedAlertingPrometheusConversionSettings{ - RuleQueryOffset: prometheusConversion.Key("rule_query_offset").MustDuration(time.Minute), + RuleQueryOffset: prometheusConversion.Key("rule_query_offset").MustDuration(time.Minute), + DefaultDatasourceUID: prometheusConversion.Key("default_datasource_uid").MustString(""), } rr := iniFile.Section("recording_rules") @@ -583,6 +591,16 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { return fmt.Errorf("setting 'deleted_rule_retention' is invalid, only 0 or a positive duration are allowed") } + uaCfg.AlertmanagerMaxTemplateOutputSize = ua.Key("alertmanager_max_template_output_bytes").MustInt64(10485760) + if uaCfg.AlertmanagerMaxTemplateOutputSize < 0 { + return fmt.Errorf("setting 'alertmanager_max_template_output_bytes' is invalid, only 0 or a positive integer are allowed") + } + + uaCfg.BacktestingMaxEvaluations = ua.Key("backtesting_max_evaluations").MustInt(100) + if uaCfg.BacktestingMaxEvaluations < 0 { + uaCfg.BacktestingMaxEvaluations = 100 + } + cfg.UnifiedAlerting = uaCfg return nil } diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 4f69daa64fd..8086151c40a 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -8,10 +8,17 @@ import ( "github.com/grafana/grafana/pkg/util/osutil" ) -var MigratedUnifiedResources = []string{ - "playlists.playlist.grafana.app", - "folders.folder.grafana.app", - "dashboards.dashboard.grafana.app", +const ( + PlaylistResource = "playlists.playlist.grafana.app" + FolderResource = "folders.folder.grafana.app" + DashboardResource = "dashboards.dashboard.grafana.app" +) + +// MigratedUnifiedResources maps resources to a boolean indicating if migration is enabled by default +var MigratedUnifiedResources = map[string]bool{ + PlaylistResource: true, // enabled by default + FolderResource: false, + DashboardResource: false, } // read storage configs from ini file. They look like: @@ -46,12 +53,19 @@ func (cfg *Cfg) setUnifiedStorageConfig() { // parse dataSyncerInterval from resource section dataSyncerInterval := section.Key("dataSyncerInterval").MustDuration(time.Hour) + // parse EnableMigration from resource section + enableMigration := MigratedUnifiedResources[resourceName] + if section.HasKey("enableMigration") { + enableMigration = section.Key("enableMigration").MustBool(MigratedUnifiedResources[resourceName]) + } + storageConfig[resourceName] = UnifiedStorageConfig{ DualWriterMode: rest.DualWriterMode(dualWriterMode), DualWriterPeriodicDataSyncJobEnabled: dualWriterPeriodicDataSyncJobEnabled, DualWriterMigrationDataSyncDisabled: dualWriterMigrationDataSyncDisabled, DataSyncerRecordsLimit: dataSyncerRecordsLimit, DataSyncerInterval: dataSyncerInterval, + EnableMigration: enableMigration, } } cfg.UnifiedStorage = storageConfig @@ -61,8 +75,8 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.DisableDataMigrations = section.Key("disable_data_migrations").MustBool(false) if !cfg.DisableDataMigrations && cfg.getUnifiedStorageType() == "unified" { // Helper log to find instances running migrations in the future - cfg.Logger.Info("Unified migration configs not yet enforced") - // cfg.enforceMigrationToUnifiedConfigs() // TODO: uncomment when ready for release + cfg.Logger.Info("Unified migration configs enforced") + cfg.enforceMigrationToUnifiedConfigs() } else { // Helper log to find instances disabling migration cfg.Logger.Info("Unified migration configs enforcement disabled", "storage_type", cfg.getUnifiedStorageType(), "disable_data_migrations", cfg.DisableDataMigrations) @@ -100,11 +114,13 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.OverridesFilePath = section.Key("overrides_path").String() cfg.OverridesReloadInterval = section.Key("overrides_reload_period").MustDuration(30 * time.Second) + // use sqlkv (resource/sqlkv) instead of the sql backend (sql/backend) as the StorageServer + cfg.EnableSQLKVBackend = section.Key("enable_sqlkv_backend").MustBool(false) + cfg.MaxFileIndexAge = section.Key("max_file_index_age").MustDuration(0) cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("") } -// nolint:unused // enforceMigrationToUnifiedConfigs enforces configurations required to run migrated resources in mode 5 // All migrated resources in MigratedUnifiedResources are set to mode 5 and unified search is enabled func (cfg *Cfg) enforceMigrationToUnifiedConfigs() { @@ -115,14 +131,22 @@ func (cfg *Cfg) enforceMigrationToUnifiedConfigs() { section.Key("enable_search").SetValue("true") cfg.EnableSearch = true } - for _, resource := range MigratedUnifiedResources { - cfg.Logger.Info("Enforcing mode 5 for resource in unified storage", "resource", resource) - if oldCfg, ok := cfg.UnifiedStorage[resource]; ok { - cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", oldCfg) + for resource, enabledByDefault := range MigratedUnifiedResources { + resourceCfg, ok := cfg.UnifiedStorage[resource] + if ok { + if !resourceCfg.EnableMigration { + cfg.Logger.Info("Resource migration disabled", "resource", resource) + continue + } + cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", resourceCfg) + } else if !enabledByDefault { + continue } + cfg.Logger.Info("Enforcing mode 5 for resource in unified storage", "resource", resource) cfg.UnifiedStorage[resource] = UnifiedStorageConfig{ DualWriterMode: 5, DualWriterMigrationDataSyncDisabled: true, + EnableMigration: true, } } } diff --git a/pkg/setting/setting_unified_storage_test.go b/pkg/setting/setting_unified_storage_test.go index 094033a4244..c112dc85962 100644 --- a/pkg/setting/setting_unified_storage_test.go +++ b/pkg/setting/setting_unified_storage_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/stretchr/testify/assert" ) @@ -13,31 +14,56 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) { err := cfg.Load(CommandLineArgs{HomePath: "../../", Config: "../../conf/defaults.ini"}) assert.NoError(t, err) - s, err := cfg.Raw.NewSection("unified_storage.playlists.playlist.grafana.app") - assert.NoError(t, err) + setSectionKey := func(sectionName, key, value string) { + section := cfg.Raw.Section(sectionName) // Gets existing or creates new + _, err := section.NewKey(key, value) + assert.NoError(t, err) + } - _, err = s.NewKey("dualWriterMode", "2") - assert.NoError(t, err) + setMigratedResourceKey := func(key, value string) { + for migratedResource := range MigratedUnifiedResources { + setSectionKey("unified_storage."+migratedResource, key, value) + } + } - _, err = s.NewKey("dualWriterPeriodicDataSyncJobEnabled", "true") - assert.NoError(t, err) + validateMigratedResources := func(optIn bool) { + for migratedResource, enabled := range MigratedUnifiedResources { + resourceCfg, exists := cfg.UnifiedStorage[migratedResource] - _, err = s.NewKey("dataSyncerRecordsLimit", "1001") - assert.NoError(t, err) + isEnabled := enabled + if optIn { + isEnabled = true + } - _, err = s.NewKey("dataSyncerInterval", "10m") - assert.NoError(t, err) + if !isEnabled { + if exists { + assert.Equal(t, rest.DualWriterMode(1), resourceCfg.DualWriterMode, migratedResource) + } + continue + } + assert.Equal(t, exists, true, migratedResource) + + assert.Equal(t, UnifiedStorageConfig{ + DualWriterMode: 5, + DualWriterMigrationDataSyncDisabled: true, + EnableMigration: isEnabled, + }, resourceCfg, migratedResource) + } + } + + setMigratedResourceKey("dualWriterMode", "1") // migrated resources enabled by default will change to 5 in setUnifiedStorageConfig + + setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dualWriterMode", "2") + setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dualWriterPeriodicDataSyncJobEnabled", "true") + setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dataSyncerRecordsLimit", "1001") + setSectionKey("unified_storage.resource.not_migrated.grafana.app", "dataSyncerInterval", "10m") // Add unified_storage section for index settings - unifiedStorageSection, err := cfg.Raw.NewSection("unified_storage") - assert.NoError(t, err) - - _, err = unifiedStorageSection.NewKey("index_min_count", "5") - assert.NoError(t, err) + setSectionKey("unified_storage", "index_min_count", "5") cfg.setUnifiedStorageConfig() - value, exists := cfg.UnifiedStorage["playlists.playlist.grafana.app"] + value, exists := cfg.UnifiedStorage["resource.not_migrated.grafana.app"] assert.Equal(t, exists, true) assert.Equal(t, value, UnifiedStorageConfig{ @@ -47,6 +73,14 @@ func TestCfg_setUnifiedStorageConfig(t *testing.T) { DataSyncerInterval: time.Minute * 10, }) + validateMigratedResources(false) + + setMigratedResourceKey("enableMigration", "true") // will be changed to 5 in setUnifiedStorageConfig + + cfg.setUnifiedStorageConfig() + + validateMigratedResources(true) + // Test that index settings are correctly parsed assert.Equal(t, 5, cfg.IndexMinCount) }) diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go index 1a06ea034ef..d30304e4bd0 100644 --- a/pkg/setting/settings_zanzana.go +++ b/pkg/setting/settings_zanzana.go @@ -110,15 +110,24 @@ func (cfg *Cfg) readZanzanaSettings() { zc.Mode = "embedded" } - zc.Token = clientSec.Key("token").MustString("") - zc.TokenExchangeURL = clientSec.Key("token_exchange_url").MustString("") 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.Token = grpcClientAuthSection.Key("token").MustString("") + zc.TokenExchangeURL = grpcClientAuthSection.Key("token_exchange_url").MustString("") zc.TokenNamespace = grpcClientAuthSection.Key("token_namespace").MustString("stacks-" + cfg.StackID) + // TODO: remove old settings when migrated + token := clientSec.Key("token").MustString("") + tokenExchangeURL := clientSec.Key("token_exchange_url").MustString("") + if token != "" { + zc.Token = token + } + if tokenExchangeURL != "" { + zc.TokenExchangeURL = tokenExchangeURL + } + cfg.ZanzanaClient = zc zs := ZanzanaServerSettings{} diff --git a/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql index c7349f27a03..92a9ca2bd58 100644 --- a/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql +++ b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql @@ -1,12 +1,13 @@ SELECT {{ .Ident "created" }}, + {{ .Ident "created_by" }}, {{ .Ident "version" }}, {{ .Ident "active" }}, {{ .Ident "namespace" }}, {{ .Ident "name" }} FROM {{ .Ident "secret_secure_value" }} -WHERE +WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND {{ .Ident "name" }} = {{ .Arg .Name }} ORDER BY {{ .Ident "version" }} DESC diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index 81ed2eb2849..32a06122b14 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -122,7 +122,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { } // toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. -func toCreateRow(createdAt, updatedAt int64, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { +func toCreateRow(createdAt, updatedAt int64, keeper string, sv *secretv1beta1.SecureValue, createdBy, updatedBy string) (*secureValueDB, error) { row, err := toRow(keeper, sv, "") if err != nil { return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err) @@ -130,9 +130,9 @@ func toCreateRow(createdAt, updatedAt int64, keeper string, sv *secretv1beta1.Se row.GUID = uuid.New().String() row.Created = createdAt - row.CreatedBy = actorUID + row.CreatedBy = createdBy row.Updated = updatedAt - row.UpdatedBy = actorUID + row.UpdatedBy = updatedBy return row, nil } diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 1fc3a788cf2..f7a4b830d27 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -85,7 +85,7 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, var row *secureValueDB err := s.db.Transaction(ctx, func(ctx context.Context) error { - latest, err := s.getLatestVersionAndCreatedAt(ctx, xkube.Namespace(sv.Namespace), sv.Name) + latest, err := s.getLatestVersionAndCreated(ctx, xkube.Namespace(sv.Namespace), sv.Name) if err != nil { return fmt.Errorf("fetching latest secure value version: %w", err) } @@ -110,7 +110,13 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, } updatedAt := now - row, err = toCreateRow(createdAt, updatedAt, keeper, sv, actorUID) + createdBy := actorUID + if latest.createdBy != "" { + createdBy = latest.createdBy + } + updatedBy := actorUID + + row, err = toCreateRow(createdAt, updatedAt, keeper, sv, createdBy, updatedBy) if err != nil { return fmt.Errorf("to create row: %w", err) } @@ -161,13 +167,14 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, return createdSecureValue, nil } -type versionAndCreatedAt struct { +type versionAndCreated struct { createdAt int64 + createdBy string version int64 } -func (s *secureValueMetadataStorage) getLatestVersionAndCreatedAt(ctx context.Context, namespace xkube.Namespace, name string) (versionAndCreatedAt, error) { - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersionAndCreatedAt", trace.WithAttributes( +func (s *secureValueMetadataStorage) getLatestVersionAndCreated(ctx context.Context, namespace xkube.Namespace, name string) (versionAndCreated, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersionAndCreated", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), )) @@ -181,45 +188,48 @@ func (s *secureValueMetadataStorage) getLatestVersionAndCreatedAt(ctx context.Co q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersionAndCreatedAt, req) if err != nil { - return versionAndCreatedAt{}, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersionAndCreatedAt.Name(), err) + return versionAndCreated{}, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersionAndCreatedAt.Name(), err) } rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...) if err != nil { - return versionAndCreatedAt{}, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) + return versionAndCreated{}, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) } defer func() { _ = rows.Close() }() if err := rows.Err(); err != nil { - return versionAndCreatedAt{}, fmt.Errorf("error executing query: %w", err) + return versionAndCreated{}, fmt.Errorf("error executing query: %w", err) } if !rows.Next() { - return versionAndCreatedAt{}, nil + return versionAndCreated{}, nil } var ( createdAt int64 + createdBy string version int64 active bool namespaceFromDB string nameFromDB string ) - if err := rows.Scan(&createdAt, &version, &active, &namespaceFromDB, &nameFromDB); err != nil { - return versionAndCreatedAt{}, fmt.Errorf("scanning version from returned rows: %w", err) + if err := rows.Scan(&createdAt, &createdBy, &version, &active, &namespaceFromDB, &nameFromDB); err != nil { + return versionAndCreated{}, fmt.Errorf("scanning version and created from returned rows: %w", err) } if namespaceFromDB != namespace.String() || nameFromDB != name { - return versionAndCreatedAt{}, fmt.Errorf("bug: expected to find latest version for namespace=%+v name=%+v but got version for namespace=%+v name=%+v", + return versionAndCreated{}, fmt.Errorf("bug: expected to find version and created for namespace=%+v name=%+v but got for namespace=%+v name=%+v", namespace, name, namespaceFromDB, nameFromDB) } if !active { createdAt = 0 + createdBy = "" } - return versionAndCreatedAt{ + return versionAndCreated{ createdAt: createdAt, + createdBy: createdBy, version: version, }, nil } diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index 2e88ee079c2..965d12a2744 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,12 +1,13 @@ SELECT `created`, + `created_by`, `version`, `active`, `namespace`, `name` FROM `secret_secure_value` -WHERE +WHERE `namespace` = 'ns' AND `name` = 'name' ORDER BY `version` DESC diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index a9f21aafc0a..67542c38e1b 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,12 +1,13 @@ SELECT "created", + "created_by", "version", "active", "namespace", "name" FROM "secret_secure_value" -WHERE +WHERE "namespace" = 'ns' AND "name" = 'name' ORDER BY "version" DESC diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index a9f21aafc0a..67542c38e1b 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,12 +1,13 @@ SELECT "created", + "created_by", "version", "active", "namespace", "name" FROM "secret_secure_value" -WHERE +WHERE "namespace" = 'ns' AND "name" = 'name' ORDER BY "version" DESC diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index 215020561a2..f2a4525c082 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -224,4 +224,23 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { mg.AddMigration("set secret_secure_value.keeper to 'system' where keeper is null in "+TableNameSecureValue, migrator.NewRawSQLMigration( fmt.Sprintf("UPDATE %s SET keeper = '%s' WHERE keeper IS NULL", TableNameSecureValue, contracts.SystemKeeperName), )) + + encryptedValueTableUniqueKey := migrator.Index{Cols: []string{"namespace", "name", "version"}, Type: migrator.UniqueIndex} + updatedEncryptedValueTable := migrator.Table{ + Name: TableNameEncryptedValue, + Columns: []*migrator.Column{ + {Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false, IsPrimaryKey: true}, // Limit enforced by K8s. + {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false, IsPrimaryKey: true}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false, IsPrimaryKey: true}, + {Name: "encrypted_data", Type: migrator.DB_Blob, Nullable: false}, + {Name: "created", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "data_key_id", Type: migrator.DB_NVarchar, Length: 100, Nullable: false, Default: "''"}, + }, + PrimaryKeys: []string{"namespace", "name", "version"}, + Indices: []*migrator.Index{ + {Cols: []string{"data_key_id"}}, + }, + } + migrator.ConvertUniqueKeyToPrimaryKey(mg, encryptedValueTableUniqueKey, updatedEncryptedValueTable) } diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 7131178e8a1..e9bdbf88e37 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -280,6 +280,35 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - make changes in `.proto` file - to compile all protobuf files in the repository run `make protobuf` at its top level +## Enable Quotas/Overrides +Quotas will make unified storage impose resource limits on a namespace. By default, the limit is 1000, but it can be overridden. To enable, create an empty overrides.yaml file in the grafana root directory. + +Then add the following to your grafana ini: +```ini +[feature_toggles] +kubernetesUnifiedStorageQuotas = true + +[unified_storage] +overrides_path = overrides.yaml +overrides_reload_period = 5s +``` + +To overrides the default quota for a tenant, add the following to the overrides.yaml file: +```yaml +overrides: + : + quotas: + .: + limit: 10 +``` +Unless otherwise set, the NAMESPACE when running locally is `default`. + +To access quotas, use the following API endpoint: +``` +GET /apis/quotas.grafana.app/v0alpha1/namespaces//usage?group=&resource= +``` + + ## Setting up search To enable it, add the following to your `custom.ini` under the `[feature_toggles]` and `[unified_storage]` sections: ```ini @@ -1317,4 +1346,34 @@ Key metrics for monitoring Unified Search: - `unified_search_shadow_requests_total`: Shadow traffic request counts - `unified_search_ring_members`: Number of active search server instances +## Data migrations +Unified storage includes an automated migration system that transfers resources from legacy SQL tables to unified storage. Migrations run automatically during Grafana startup when enabled. + +### Supported resources + +- Folders +- Dashboards +- Library panels +- Playlists + +### Validation + +Built-in validators ensure data integrity after migration: + +- **CountValidator**: Verifies resource counts match between legacy and unified storage +- **FolderTreeValidator**: Validates folder parent-child relationships are preserved + +### Configuration + +Enable migrations in `grafana.ini`: + +```ini +[unified_storage] +disable_data_migrations = false +``` + +### Documentation + +For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md). + \ No newline at end of file diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go index 0b36f71117a..a35c2398736 100644 --- a/pkg/storage/unified/apistore/prepare_test.go +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -2,6 +2,7 @@ package apistore import ( "context" + "encoding/json" "math/rand/v2" "strings" "testing" @@ -19,6 +20,7 @@ import ( authlib "github.com/grafana/authlib/types" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -195,6 +197,42 @@ func TestPrepareObjectForStorage(t *testing.T) { require.Equal(t, int64(2), meta2.GetGeneration()) }) + t.Run("Update should skip incrementing generation when content is unchanged", func(t *testing.T) { + dashboard := dashv1.Dashboard{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Generation: 123, + Annotations: map[string]string{ + "A": "B", + utils.AnnoKeyUpdatedTimestamp: "2025-12-17T01:01:00Z", + }, + UID: "XXX", + }, + Spec: v0alpha1.Unstructured{ + Object: map[string]any{ + "hello": "world", + }, + }, + } + dashboard.Name = "test-name" + obj := dashboard.DeepCopyObject() + tmp, err := utils.MetaAccessor(obj) + tmp.SetGeneration(2) + tmp.SetUpdatedTimestampMillis(12345) + require.NoError(t, err) + + v, err := s.prepareObjectForUpdate(ctx, obj, &dashboard) + require.NoError(t, err) + require.False(t, v.hasChanged, "no changes") + + out := &unstructured.Unstructured{} + err = json.Unmarshal(v.raw.Bytes(), out) + require.NoError(t, err) + + require.Equal(t, int64(123), tmp.GetGeneration()) + require.Equal(t, "2025-12-17T01:01:00Z", tmp.GetAnnotation(utils.AnnoKeyUpdatedTimestamp)) + }) + s.opts.RequireDeprecatedInternalID = true t.Run("Should generate internal id", func(t *testing.T) { dashboard := dashv1.Dashboard{} diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 495efc32175..5d7c5a372a0 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -166,6 +166,7 @@ type resourceClientMock struct { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } // always return GRPC Unauthenticated code diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 45130c8a6a6..82336b3a5d1 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -168,7 +168,7 @@ func newClient(opts options.StorageOptions, return resource.NewResourceClient(conn, indexConn, cfg, features, tracer) default: - searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, indexMetrics, nil) + searchOptions, err := search.NewSearchOptions(features, cfg, docs, indexMetrics, nil) if err != nil { return nil, err } diff --git a/pkg/storage/unified/migrations/README.md b/pkg/storage/unified/migrations/README.md new file mode 100644 index 00000000000..b0c84d81678 --- /dev/null +++ b/pkg/storage/unified/migrations/README.md @@ -0,0 +1,122 @@ +# Unified storage data migrations + +Automated migration system for moving Grafana resources from legacy SQL storage to unified storage. + +## Overview + +The migration system transfers resources from legacy SQL tables to Grafana's unified storage backend. It runs automatically during Grafana startup and validates data integrity after each migration. + +### Supported resources + +| Resource | API Group | Legacy table | +|----------|-----------|--------------| +| Folders | `folder.grafana.app` | `dashboard` | +| Dashboards | `dashboard.grafana.app` | `dashboard` | +| Library panels | `dashboard.grafana.app` | `library_element` | +| Playlists | `playlist.grafana.app` | `playlist` | + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ResourceMigration │ +│ (Orchestrates per-organization migration) │ +└──────────────────────────┬──────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ + UnifiedMigrator Validators BulkProcess API + (Stream legacy (Validate after (Write to unified + resources) migration) storage) +``` + +### Components + +- **`service.go`**: Migration service entry point and registration +- **`migrator.go`**: Core migration logic using streaming BulkProcess API +- **`resource_migration.go`**: Per-organization migration execution +- **`validator.go`**: Post-migration validation (CountValidator, FolderTreeValidator) +- **`resources.go`**: Registry of migratable resource types + +## How migrations work + +### Migration flow + +1. Grafana starts and checks migration status in `unifiedstorage_migration_log` table +2. For each organization, the migrator: + - Reads resources from legacy SQL tables + - Streams resources to unified storage via BulkProcess API + - Runs validators to verify data integrity +3. Records migration result in `unifiedstorage_migration_log` table + +### Per-organization execution + +Migrations run independently for each organization using namespace format `org-{orgId}`. + +## Validators + +### CountValidator + +Compares resource counts between legacy SQL and unified storage. Accounts for rejected items during validation. + +### FolderTreeValidator + +Verifies folder parent-child relationships are preserved after migration. + +## Configuration + +To enable migrations, set the following in your Grafana configuration: + +```ini +[unified_storage] +disable_data_migrations = false +``` + +## Monitoring + +### Log messages + +Successful migration: + +``` +info: storage.unified.resource_migration Starting migration for all organizations +info: storage.unified.resource_migration Migration completed successfully for all organizations +``` + +Failed migration: + +``` +error: storage.unified.resource_migration Migration validation failed +``` + +### Migration status + +Query the migration log table to check status: + +```sql +SELECT * FROM unifiedstorage_migration_log WHERE migration_id LIKE '%folders-dashboards%'; +``` + +The `migration_id` is defined in `service.go` during registration. Ideally, it should be the resource type(s) being migrated. + +## Development + +### Adding a new validator + +Implement the `Validator` interface: + +```go +type Validator interface { + Name() string + Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error +} +``` + +Register the validator in `service.go` when creating the `ResourceMigration`. + +### Adding a new resource type + +1. Add the resource definition to `registeredResources` in `resources.go` +2. Implement the migrator function in the `MigrationDashboardAccessor` interface +3. Register the migration in `service.go` + diff --git a/pkg/storage/unified/migrations/migrator_test.go b/pkg/storage/unified/migrations/migrator_test.go index 92afe698a98..8552092d066 100644 --- a/pkg/storage/unified/migrations/migrator_test.go +++ b/pkg/storage/unified/migrations/migrator_test.go @@ -162,14 +162,49 @@ func runMigrationTestSuite(t *testing.T, testCases []resourceMigratorTestCase) { } }) - t.Run("Step 3: verify data is migrated to unified storage", func(t *testing.T) { - // Migrations will run automatically at startup and mode 5 is enforced by the config + t.Run("Step 3: verify that opted-out resources are not migrated", func(t *testing.T) { + // Build unified storage config for Mode5 + unifiedConfig := make(map[string]setting.UnifiedStorageConfig) + for _, tc := range testCases { + for _, gvr := range tc.resources() { + resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group) + unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{ + DualWriterMode: grafanarest.Mode5, + EnableMigration: false, + } + } + } + + helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{ + GrafanaOpts: testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + DisableDBCleanup: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: unifiedConfig, + }, + Org1Users: org1, + OrgBUsers: orgB, + }) + t.Cleanup(helper.Shutdown) + + for _, state := range testStates { + t.Run(state.tc.name(), func(t *testing.T) { + // Verify resources don't exist in unified storage yet + state.tc.verify(t, helper, false) + }) + } + verifyRegisteredMigrations(t, helper, false, true) + }) + + t.Run("Step 4: verify data is migrated to unified storage", func(t *testing.T) { + // Migrations enabled by default will run automatically at startup and mode 5 is enforced by the config helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{ GrafanaOpts: testinfra.GrafanaOpts{ - // EnableLog: true, AppModeProduction: true, DisableAnonymous: true, DisableDataMigrations: false, // Run migrations at startup + DisableDBCleanup: true, APIServerStorageType: "unified", }, Org1Users: org1, @@ -183,7 +218,89 @@ func runMigrationTestSuite(t *testing.T, testCases []resourceMigratorTestCase) { state.tc.verify(t, helper, true) }) } + + t.Logf("Verifying migrations are correctly registered") + verifyRegisteredMigrations(t, helper, true, false) }) + + t.Run("Step 5: verify data is migrated for all migrations", func(t *testing.T) { + // Trigger migrations that are not enabled by default + unifiedConfig := make(map[string]setting.UnifiedStorageConfig) + for _, tc := range testCases { + for _, gvr := range tc.resources() { + resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group) + unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{ + EnableMigration: true, + } + } + } + helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{ + GrafanaOpts: testinfra.GrafanaOpts{ + // EnableLog: true, + AppModeProduction: true, + DisableAnonymous: true, + DisableDataMigrations: false, + APIServerStorageType: "unified", + UnifiedStorageConfig: unifiedConfig, + }, + Org1Users: org1, + OrgBUsers: orgB, + }) + t.Cleanup(helper.Shutdown) + + for _, state := range testStates { + t.Run(state.tc.name(), func(t *testing.T) { + // Verify resources still exist in unified storage after restart + state.tc.verify(t, helper, true) + }) + } + + t.Logf("Verifying migrations are correctly registered") + verifyRegisteredMigrations(t, helper, false, false) + }) +} + +const ( + migrationScope = "unifiedstorage" + migrationTable = migrationScope + "_migration_log" + + playlistsID = "playlists migration" + foldersAndDashboardsID = "folders and dashboards migration" +) + +var migrationIDsToDefault = map[string]bool{ + playlistsID: true, + foldersAndDashboardsID: false, +} + +func verifyRegisteredMigrations(t *testing.T, helper *apis.K8sTestHelper, onlyDefault bool, optOut bool) { + getMigrationsQuery := fmt.Sprintf("SELECT migration_id FROM %s", migrationTable) + createTableMigrationID := fmt.Sprintf("create %s table", migrationTable) + expectedMigrationIDs := []string{createTableMigrationID} + for id, enabled := range migrationIDsToDefault { + if onlyDefault && !enabled { + continue + } + if optOut { + continue + } + expectedMigrationIDs = append(expectedMigrationIDs, id) + } + rows, err := helper.GetEnv().SQLStore.GetEngine().DB().Query(getMigrationsQuery) + require.NoError(t, err) + defer func() { + require.NoError(t, rows.Close()) + }() + + migrationIDs := make(map[string]struct{}) + for rows.Next() { + var migrationID string + require.NoError(t, rows.Scan(&migrationID)) + require.Contains(t, expectedMigrationIDs, migrationID) + migrationIDs[migrationID] = struct{}{} + } + require.NoError(t, rows.Err()) + require.Len(t, migrationIDs, len(expectedMigrationIDs)) } // verifyResourceCount verifies that the expected number of resources exist in K8s storage diff --git a/pkg/storage/unified/migrations/resources.go b/pkg/storage/unified/migrations/resources.go index 0a4dcc7766e..ea32bb54322 100644 --- a/pkg/storage/unified/migrations/resources.go +++ b/pkg/storage/unified/migrations/resources.go @@ -7,7 +7,9 @@ import ( folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" playlists "github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -17,7 +19,13 @@ type ResourceDefinition struct { MigratorFunc string // Name of the method: "MigrateFolders", "MigrateDashboards", etc. } -var registeredResources = []ResourceDefinition{ +type migrationDefinition struct { + name string + resources []string + registerFunc func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) +} + +var resourceRegistry = []ResourceDefinition{ { GroupResource: schema.GroupResource{Group: folders.GROUP, Resource: folders.RESOURCE}, MigratorFunc: "MigrateFolders", @@ -36,9 +44,50 @@ var registeredResources = []ResourceDefinition{ }, } +var migrationRegistry = []migrationDefinition{ + { + name: "playlists", + resources: []string{setting.PlaylistResource}, + registerFunc: registerPlaylistMigration, + }, + { + name: "folders and dashboards", + resources: []string{setting.FolderResource, setting.DashboardResource}, + registerFunc: registerDashboardAndFolderMigration, + }, +} + +func registerMigrations(cfg *setting.Cfg, mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) error { + for _, migration := range migrationRegistry { + var ( + hasValue bool + allEnabled bool + ) + + for _, res := range migration.resources { + enabled := cfg.UnifiedStorage[res].EnableMigration + if !hasValue { + allEnabled = enabled + hasValue = true + continue + } + if enabled != allEnabled { + return fmt.Errorf("cannot migrate resources separately: %v migration must be either all enabled or all disabled", migration.resources) + } + } + + if !allEnabled { + logger.Info("Migration is disabled in config, skipping", "migration", migration.name) + continue + } + migration.registerFunc(mg, migrator, client) + } + return nil +} + func getResourceDefinition(group, resource string) *ResourceDefinition { - for i := range registeredResources { - r := ®isteredResources[i] + for i := range resourceRegistry { + r := &resourceRegistry[i] if r.GroupResource.Group == group && r.GroupResource.Resource == resource { return r } @@ -80,13 +129,13 @@ func getMigratorFunc(accessor legacy.MigrationDashboardAccessor, group, resource func validateRegisteredResources() error { registeredMap := make(map[string]bool) - for _, gr := range registeredResources { + for _, gr := range resourceRegistry { key := fmt.Sprintf("%s.%s", gr.GroupResource.Resource, gr.GroupResource.Group) registeredMap[key] = true } var missing []string - for _, expected := range setting.MigratedUnifiedResources { + for expected := range setting.MigratedUnifiedResources { if !registeredMap[expected] { missing = append(missing, expected) } diff --git a/pkg/storage/unified/migrations/resources_test.go b/pkg/storage/unified/migrations/resources_test.go new file mode 100644 index 00000000000..5a519e9ad62 --- /dev/null +++ b/pkg/storage/unified/migrations/resources_test.go @@ -0,0 +1,92 @@ +package migrations + +import ( + "testing" + + sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" +) + +// TestRegisterMigrations exercises registerMigrations with various EnableMigration configs using a table-driven test. +func TestRegisterMigrations(t *testing.T) { + origRegistry := migrationRegistry + t.Cleanup(func() { migrationRegistry = origRegistry }) + + // helper to build a fake registry with custom register funcs that bump counters + makeFakeRegistry := func(migrationCalls map[string]int) []migrationDefinition { + return []migrationDefinition{ + { + name: "playlists", + resources: []string{setting.PlaylistResource}, + registerFunc: func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { + migrationCalls["playlists"]++ + }, + }, + { + name: "folders and dashboards", + resources: []string{setting.FolderResource, setting.DashboardResource}, + registerFunc: func(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { + migrationCalls["folders and dashboards"]++ + }, + }, + } + } + + // Build a minimal cfg with UnifiedStorage entries used by registerMigrations + makeCfg := func(vals map[string]bool) *setting.Cfg { + cfg := &setting.Cfg{UnifiedStorage: make(map[string]setting.UnifiedStorageConfig)} + for k, v := range vals { + cfg.UnifiedStorage[k] = setting.UnifiedStorageConfig{EnableMigration: v} + } + return cfg + } + + // Table of scenarios + tests := []struct { + name string + enablePlaylist bool + enableFolder bool + enableDashboard bool + wantPlaylistCalls int + wantFDCalls int + wantErr bool + }{ + {name: "playlists enabled", enablePlaylist: true, wantPlaylistCalls: 1}, + {name: "playlists disabled", enablePlaylist: false, wantPlaylistCalls: 0}, + {name: "folders+dashboards both enabled", enableFolder: true, enableDashboard: true, wantFDCalls: 1}, + {name: "folders enabled, dashboards disabled (mismatch)", enableFolder: true, enableDashboard: false, wantFDCalls: 0, wantErr: true}, + {name: "folders disabled, dashboards enabled (mismatch)", enableFolder: false, enableDashboard: true, wantFDCalls: 0, wantErr: true}, + {name: "folders+dashboards both disabled", enableFolder: false, enableDashboard: false, wantFDCalls: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + migrationCalls := map[string]int{ + "playlists": 0, + "folders and dashboards": 0, + } + + migrationRegistry = makeFakeRegistry(migrationCalls) + + cfg := makeCfg(map[string]bool{ + setting.PlaylistResource: tt.enablePlaylist, + setting.FolderResource: tt.enableFolder, + setting.DashboardResource: tt.enableDashboard, + }) + + // We pass nils for migrator dependencies because our fake registerFuncs don't use them + err := registerMigrations(cfg, nil, nil, nil) + + if tt.wantErr { + require.Error(t, err, "expected error for mismatched enablement") + } else { + require.NoError(t, err, "unexpected error") + } + + require.Equal(t, tt.wantPlaylistCalls, migrationCalls["playlists"], "playlists register call count") + require.Equal(t, tt.wantFDCalls, migrationCalls["folders and dashboards"], "folders+dashboards register call count") + }) + } +} diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 1fda9b42593..4ac9cdad218 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -3,7 +3,6 @@ package migrations import ( "context" "fmt" - "os" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" @@ -49,34 +48,25 @@ func ProvideUnifiedStorageMigrationService( } func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { - // TODO: temporary skip migrations in test environments to prevent integration test timeouts. - if os.Getenv("GRAFANA_TEST_DB") != "" { - return nil - } - // skip migrations if disabled in config if p.cfg.DisableDataMigrations { metrics.MUnifiedStorageMigrationStatus.Set(1) logger.Info("Data migrations are disabled, skipping") return nil - } else { - metrics.MUnifiedStorageMigrationStatus.Set(2) - logger.Info("Data migrations not yet enforced, skipping") } - - // TODO: Re-enable once migrations are ready - // TODO: add guarantee that this only runs once - // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore, p.client) - return nil + logger.Info("Running migrations for unified storage") + metrics.MUnifiedStorageMigrationStatus.Set(3) + return RegisterMigrations(ctx, p.migrator, p.cfg, p.sqlStore, p.client) } func RegisterMigrations( + ctx context.Context, migrator UnifiedMigrator, cfg *setting.Cfg, sqlStore db.DB, client resource.ResourceClient, ) error { - ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") + ctx, span := tracer.Start(ctx, "storage.unified.RegisterMigrations") defer span.End() mg := sqlstoremigrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unifiedstorage") mg.AddCreateMigration() @@ -89,12 +79,19 @@ func RegisterMigrations( return err } - // Register resource migrations - registerDashboardAndFolderMigration(mg, migrator, client) - registerPlaylistMigration(mg, migrator, client) + if err := registerMigrations(cfg, mg, migrator, client); err != nil { + return err + } // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") + db := mg.DBEngine.DB().DB + maxOpenConns := db.Stats().MaxOpenConnections + if maxOpenConns <= 2 { + // migrations require at least 3 connections due to extra GRPC connections + db.SetMaxOpenConns(3) + defer db.SetMaxOpenConns(maxOpenConns) + } if err := mg.RunMigrations(ctx, sec.Key("migration_locking").MustBool(true), sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { diff --git a/pkg/storage/unified/proto/resource.proto b/pkg/storage/unified/proto/resource.proto index 91a9288b194..ca91d9efd21 100644 --- a/pkg/storage/unified/proto/resource.proto +++ b/pkg/storage/unified/proto/resource.proto @@ -587,6 +587,22 @@ message ResourceTableRow { bytes object = 4; } +message QuotaUsageRequest { + // Namespace (tenant) + ResourceKey key = 1; +} + +message QuotaUsageResponse { + // Error details + ErrorResult error = 1; + + // Current usage + int64 usage = 2; + + // Current limit + int64 limit = 3; +} + // This provides the CRUD+List+Watch support needed for a k8s apiserver // The semantics and behaviors of this service are constrained by kubernetes // This does not understand the resource schemas, only deals with json bytes @@ -631,3 +647,8 @@ service Diagnostics { // Check if the service is healthy rpc IsHealthy(HealthCheckRequest) returns (HealthCheckResponse); } + +service Quotas { + // Get current quota usage and limits + rpc GetQuotaUsage(QuotaUsageRequest) returns (QuotaUsageResponse); +} diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index 58a9d8b8b76..c0ddd9e6613 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -10,7 +10,6 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" claims "github.com/grafana/authlib/types" @@ -77,21 +76,16 @@ type authzLimitedClient struct { // allowlist is a map of group to resources that are compatible with RBAC. allowlist groupResource logger log.Logger - tracer trace.Tracer metrics *accessMetrics } type AuthzOptions struct { - Tracer trace.Tracer Registry prometheus.Registerer } // NewAuthzLimitedClient creates a new authzLimitedClient. func NewAuthzLimitedClient(client claims.AccessClient, opts AuthzOptions) claims.AccessClient { logger := log.New("limited-authz-client") - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("limited-authz-client") - } if opts.Registry == nil { opts.Registry = prometheus.DefaultRegisterer } @@ -102,7 +96,6 @@ func NewAuthzLimitedClient(client claims.AccessClient, opts AuthzOptions) claims "folder.grafana.app": map[string]interface{}{"folders": nil}, }, logger: logger, - tracer: opts.Tracer, metrics: newMetrics(opts.Registry), } } @@ -110,7 +103,7 @@ func NewAuthzLimitedClient(client claims.AccessClient, opts AuthzOptions) claims // Check implements claims.AccessClient. func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req claims.CheckRequest, folder string) (claims.CheckResponse, error) { t := time.Now() - ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Check", trace.WithAttributes( + ctx, span := tracer.Start(ctx, "resource.authzLimitedClient.Check", trace.WithAttributes( attribute.String("group", req.Group), attribute.String("resource", req.Resource), attribute.String("namespace", req.Namespace), @@ -163,7 +156,7 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, claims.Zookie, error) { t := time.Now() fallbackUsed := FallbackUsed(ctx) - ctx, span := c.tracer.Start(ctx, "authzLimitedClient.Compile", trace.WithAttributes( + ctx, span := tracer.Start(ctx, "resource.authzLimitedClient.Compile", trace.WithAttributes( attribute.String("group", req.Group), attribute.String("resource", req.Resource), attribute.String("namespace", req.Namespace), diff --git a/pkg/storage/unified/resource/cdk_blob.go b/pkg/storage/unified/resource/cdk_blob.go index e9dbdb22834..cfd8f32fda1 100644 --- a/pkg/storage/unified/resource/cdk_blob.go +++ b/pkg/storage/unified/resource/cdk_blob.go @@ -11,8 +11,6 @@ import ( "time" "github.com/google/uuid" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" "gocloud.dev/blob" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -27,7 +25,6 @@ import ( ) type CDKBlobSupportOptions struct { - Tracer trace.Tracer Bucket CDKBucket RootFolder string URLExpiration time.Duration @@ -47,10 +44,6 @@ func OpenBlobBucket(ctx context.Context, url string) (*blob.Bucket, error) { } func NewCDKBlobSupport(ctx context.Context, opts CDKBlobSupportOptions) (BlobSupport, error) { - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("cdk-blob-store") - } - if opts.Bucket == nil { return nil, fmt.Errorf("missing bucket") } @@ -70,7 +63,6 @@ func NewCDKBlobSupport(ctx context.Context, opts CDKBlobSupportOptions) (BlobSup } return &cdkBlobSupport{ - tracer: opts.Tracer, bucket: opts.Bucket, root: opts.RootFolder, cansignurls: false, // TODO depends on the implementation @@ -79,7 +71,6 @@ func NewCDKBlobSupport(ctx context.Context, opts CDKBlobSupportOptions) (BlobSup } type cdkBlobSupport struct { - tracer trace.Tracer bucket CDKBucket root string cansignurls bool diff --git a/pkg/storage/unified/resource/cdk_bucket.go b/pkg/storage/unified/resource/cdk_bucket.go index e148326f8a9..8f7dd6facd2 100644 --- a/pkg/storage/unified/resource/cdk_bucket.go +++ b/pkg/storage/unified/resource/cdk_bucket.go @@ -7,7 +7,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" "gocloud.dev/blob" ) @@ -36,14 +35,12 @@ const ( type InstrumentedBucket struct { requests *prometheus.CounterVec latency *prometheus.HistogramVec - tracer trace.Tracer bucket CDKBucket } -func NewInstrumentedBucket(bucket CDKBucket, reg prometheus.Registerer, tracer trace.Tracer) *InstrumentedBucket { +func NewInstrumentedBucket(bucket CDKBucket, reg prometheus.Registerer) *InstrumentedBucket { b := &InstrumentedBucket{ bucket: bucket, - tracer: tracer, } b.initMetrics(reg) return b @@ -66,7 +63,7 @@ func (b *InstrumentedBucket) initMetrics(reg prometheus.Registerer) { } func (b *InstrumentedBucket) Attributes(ctx context.Context, key string) (*blob.Attributes, error) { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/Attributes") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.Attributes") defer span.End() start := time.Now() retVal, err := b.bucket.Attributes(ctx, key) @@ -94,7 +91,7 @@ func (b *InstrumentedBucket) List(opts *blob.ListOptions) *blob.ListIterator { } func (b *InstrumentedBucket) ListPage(ctx context.Context, pageToken []byte, pageSize int, opts *blob.ListOptions) ([]*blob.ListObject, []byte, error) { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/ListPage") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.ListPage") defer span.End() start := time.Now() retVal, nextPageToken, err := b.bucket.ListPage(ctx, pageToken, pageSize, opts) @@ -116,7 +113,7 @@ func (b *InstrumentedBucket) ListPage(ctx context.Context, pageToken []byte, pag } func (b *InstrumentedBucket) ReadAll(ctx context.Context, key string) ([]byte, error) { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/ReadAll") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.ReadAll") defer span.End() start := time.Now() retVal, err := b.bucket.ReadAll(ctx, key) @@ -139,7 +136,7 @@ func (b *InstrumentedBucket) ReadAll(ctx context.Context, key string) ([]byte, e } func (b *InstrumentedBucket) WriteAll(ctx context.Context, key string, p []byte, opts *blob.WriterOptions) error { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/WriteAll") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.WriteAll") defer span.End() start := time.Now() err := b.bucket.WriteAll(ctx, key, p, opts) @@ -162,7 +159,7 @@ func (b *InstrumentedBucket) WriteAll(ctx context.Context, key string, p []byte, } func (b *InstrumentedBucket) Delete(ctx context.Context, key string) error { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/Delete") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.Delete") defer span.End() start := time.Now() err := b.bucket.Delete(ctx, key) @@ -185,7 +182,7 @@ func (b *InstrumentedBucket) Delete(ctx context.Context, key string) error { } func (b *InstrumentedBucket) SignedURL(ctx context.Context, key string, opts *blob.SignedURLOptions) (string, error) { - ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/SignedURL") + ctx, span := tracer.Start(ctx, "resource.InstrumentedBucket.SignedURL") defer span.End() start := time.Now() retVal, err := b.bucket.SignedURL(ctx, key, opts) diff --git a/pkg/storage/unified/resource/cdk_bucket_test.go b/pkg/storage/unified/resource/cdk_bucket_test.go index 9a577a23864..75fd799fbcb 100644 --- a/pkg/storage/unified/resource/cdk_bucket_test.go +++ b/pkg/storage/unified/resource/cdk_bucket_test.go @@ -8,7 +8,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel" "gocloud.dev/blob" ) @@ -196,8 +195,7 @@ func TestInstrumentedBucket(t *testing.T) { t.Run(op.name+" "+tc.name, func(t *testing.T) { fakeBucket := &fakeCDKBucket{} reg := prometheus.NewPedanticRegistry() - tracer := otel.Tracer("test") - instrumentedBucket := NewInstrumentedBucket(fakeBucket, reg, tracer) + instrumentedBucket := NewInstrumentedBucket(fakeBucket, reg) op.setup(fakeBucket, tc.success) err := op.call(instrumentedBucket) diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index b2742f71dc9..e51b7ec4876 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -39,6 +39,7 @@ type ResourceClient interface { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } // Internal implementation @@ -49,6 +50,7 @@ type resourceClient struct { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } func NewResourceClient(conn, indexConn grpc.ClientConnInterface, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer trace.Tracer) (ResourceClient, error) { @@ -76,6 +78,7 @@ func newResourceClient(storageCc grpc.ClientConnInterface, indexCc grpc.ClientCo BulkStoreClient: resourcepb.NewBulkStoreClient(storageCc), BlobStoreClient: resourcepb.NewBlobStoreClient(storageCc), DiagnosticsClient: resourcepb.NewDiagnosticsClient(storageCc), + QuotasClient: resourcepb.NewQuotasClient(storageCc), } } @@ -102,6 +105,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { &resourcepb.BlobStore_ServiceDesc, &resourcepb.BulkStore_ServiceDesc, &resourcepb.Diagnostics_ServiceDesc, + &resourcepb.Quotas_ServiceDesc, } { channel.RegisterService( grpchan.InterceptServer( diff --git a/pkg/storage/unified/resource/client_mock.go b/pkg/storage/unified/resource/client_mock.go index fcc7392880e..059421b487a 100644 --- a/pkg/storage/unified/resource/client_mock.go +++ b/pkg/storage/unified/resource/client_mock.go @@ -394,6 +394,80 @@ func (_c *MockResourceClient_GetBlob_Call) RunAndReturn(run func(context.Context return _c } +// GetQuotaUsage provides a mock function with given fields: ctx, in, opts +func (_m *MockResourceClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetQuotaUsage") + } + + var r0 *resourcepb.QuotaUsageResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) *resourcepb.QuotaUsageResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.QuotaUsageResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockResourceClient_GetQuotaUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotaUsage' +type MockResourceClient_GetQuotaUsage_Call struct { + *mock.Call +} + +// GetQuotaUsage is a helper method to define mock.On call +// - ctx context.Context +// - in *resourcepb.QuotaUsageRequest +// - opts ...grpc.CallOption +func (_e *MockResourceClient_Expecter) GetQuotaUsage(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_GetQuotaUsage_Call { + return &MockResourceClient_GetQuotaUsage_Call{Call: _e.mock.On("GetQuotaUsage", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) Run(run func(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption)) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*resourcepb.QuotaUsageRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) Return(_a0 *resourcepb.QuotaUsageResponse, _a1 error) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) RunAndReturn(run func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Return(run) + return _c +} + // GetStats provides a mock function with given fields: ctx, in, opts func (_m *MockResourceClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/pkg/storage/unified/resource/data/sqlkv_batch_delete.sql b/pkg/storage/unified/resource/data/sqlkv_batch_delete.sql new file mode 100644 index 00000000000..c165007d23d --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_batch_delete.sql @@ -0,0 +1,7 @@ +DELETE +FROM {{ .TableName }} +WHERE {{ .Ident "key_path" }} IN ( + {{ range $id, $key_path := .KeyPaths }} + {{ if ne $id 0 }}, {{ end }}{{ $.Arg $key_path }} + {{ end }} +); diff --git a/pkg/storage/unified/resource/data/sqlkv_batch_get.sql b/pkg/storage/unified/resource/data/sqlkv_batch_get.sql new file mode 100644 index 00000000000..0babcb36970 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_batch_get.sql @@ -0,0 +1,12 @@ +SELECT r.{{ .Ident "key_path" }}, r.{{ .Ident "value" }} +FROM ( +{{ range $id, $key_path := .KeyPaths }} + {{ if eq $id 0 }} + SELECT {{ $.Arg $id }} AS idx, {{ $.Arg $key_path }} AS key_path + {{ else }} + UNION ALL SELECT {{ $.Arg $id }}, {{ $.Arg $key_path }} + {{ end }} +{{ end }} +) AS requested_keys +INNER JOIN {{ .TableName }} r ON r.{{ .Ident "key_path" }} = requested_keys.{{ .Ident "key_path" }} +ORDER BY requested_keys.{{ .Ident "idx" }}; diff --git a/pkg/storage/unified/resource/data/sqlkv_delete.sql b/pkg/storage/unified/resource/data/sqlkv_delete.sql new file mode 100644 index 00000000000..60a60746993 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_delete.sql @@ -0,0 +1,3 @@ +DELETE +FROM {{ .Ident .TableName }} +WHERE {{ .Ident "key_path" }} = {{ .Arg .KeyPath }}; diff --git a/pkg/storage/unified/resource/data/sqlkv_delete_legacy_resource.sql b/pkg/storage/unified/resource/data/sqlkv_delete_legacy_resource.sql new file mode 100644 index 00000000000..e27ee578a41 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_delete_legacy_resource.sql @@ -0,0 +1,5 @@ +DELETE FROM {{ .Ident "resource" }} +WHERE {{ .Ident "group" }} = {{ .Arg .Group }} +AND {{ .Ident "resource" }} = {{ .Arg .Resource }} +AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +AND {{ .Ident "name" }} = {{ .Arg .Name }}; diff --git a/pkg/storage/unified/resource/data/sqlkv_get.sql b/pkg/storage/unified/resource/data/sqlkv_get.sql new file mode 100644 index 00000000000..48c83cf0450 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_get.sql @@ -0,0 +1,3 @@ +SELECT {{ .Ident "value" | .Into .Value }} +FROM {{ .Ident .TableName }} +WHERE {{ .Ident "key_path" }} = {{ .Arg .KeyPath }}; diff --git a/pkg/storage/unified/resource/data/sqlkv_insert_datastore.sql b/pkg/storage/unified/resource/data/sqlkv_insert_datastore.sql new file mode 100644 index 00000000000..8372eb73463 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_insert_datastore.sql @@ -0,0 +1,21 @@ +INSERT INTO {{ .Ident .TableName }} +( + {{ .Ident "guid" }}, + {{ .Ident "key_path" }}, + {{ .Ident "value" }}, + {{ .Ident "group" }}, + {{ .Ident "resource" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "action" }} +) +VALUES ( + {{ .Arg .GUID }}, + {{ .Arg .KeyPath }}, + COALESCE({{ .Arg .Value }}, ""), + {{ .Arg .Group }}, + {{ .Arg .Resource }}, + {{ .Arg .Namespace }}, + {{ .Arg .Name }}, + {{ .Arg .Action }} +); diff --git a/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource.sql b/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource.sql new file mode 100644 index 00000000000..1f58bd28b43 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource.sql @@ -0,0 +1,31 @@ +INSERT INTO {{ .Ident "resource" }} +( + {{ .Ident "value" }}, + {{ .Ident "guid" }}, + {{ .Ident "group" }}, + {{ .Ident "resource" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "action" }}, + {{ .Ident "folder" }}, + {{ .Ident "previous_resource_version" }} +) +VALUES ( + COALESCE({{ .Arg .Value }}, ""), + {{ .Arg .GUID }}, + {{ .Arg .Group }}, + {{ .Arg .Resource }}, + {{ .Arg .Namespace }}, + {{ .Arg .Name }}, + {{ .Arg .Action }}, + {{ .Arg .Folder }}, + CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE ( + SELECT {{ .Ident "resource_version" }} + FROM {{ .Ident "resource" }} + WHERE {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} + AND {{ .Ident "name" }} = {{ .Arg .Name }} + ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1 + ) END +); diff --git a/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource_history.sql b/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource_history.sql new file mode 100644 index 00000000000..d52aac5063d --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_insert_legacy_resource_history.sql @@ -0,0 +1,44 @@ +INSERT INTO {{ .Ident "resource_history" }} +( + {{ .Ident "value" }}, + {{ .Ident "guid" }}, + {{ .Ident "group" }}, + {{ .Ident "resource" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "action" }}, + {{ .Ident "folder" }}, + {{ .Ident "previous_resource_version" }}, + {{ .Ident "generation" }} +) +VALUES ( + COALESCE({{ .Arg .Value }}, ""), + {{ .Arg .GUID }}, + {{ .Arg .Group }}, + {{ .Arg .Resource }}, + {{ .Arg .Namespace }}, + {{ .Arg .Name }}, + {{ .Arg .Action }}, + {{ .Arg .Folder }}, + CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE ( + SELECT {{ .Ident "resource_version" }} + FROM {{ .Ident "resource_history" }} + WHERE {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} + AND {{ .Ident "name" }} = {{ .Arg .Name }} + ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1 + ) END, + CASE + WHEN {{ .Arg .Action }} = 1 THEN 1 + WHEN {{ .Arg .Action }} = 3 THEN 0 + ELSE 1 + ( + SELECT COUNT(1) + FROM {{ .Ident "resource_history" }} + WHERE {{ .Ident "group" }} = {{ .Arg .Group }} + AND {{ .Ident "resource" }} = {{ .Arg .Resource }} + AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} + AND {{ .Ident "name" }} = {{ .Arg .Name }} + ) + END +); diff --git a/pkg/storage/unified/resource/data/sqlkv_keys.sql b/pkg/storage/unified/resource/data/sqlkv_keys.sql new file mode 100644 index 00000000000..36b7bfbe01c --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_keys.sql @@ -0,0 +1,9 @@ +SELECT {{ .Ident "key_path" }} +FROM {{ .TableName }} +WHERE {{ .Ident "key_path" }} >= {{ .Arg .StartKey }} + AND {{ .Ident "key_path" }} < {{ .Arg .EndKey }} +ORDER BY {{ .Ident "key_path" }} {{ if .SortAscending }}ASC{{ else }}DESC{{ end }} +{{ if .Options.Limit }} +LIMIT {{ .Options.Limit }} +{{ end }} +; diff --git a/pkg/storage/unified/resource/data/sqlkv_save_event.sql b/pkg/storage/unified/resource/data/sqlkv_save_event.sql new file mode 100644 index 00000000000..669497dbb19 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_save_event.sql @@ -0,0 +1,15 @@ +INSERT INTO {{ .Ident .TableName }} +( + {{ .Ident "key_path" }}, + {{ .Ident "value" }} +) +VALUES ( + {{ .Arg .KeyPath }}, + COALESCE({{ .Arg .Value }}, "") +) +{{- if eq .DialectName "mysql" }} +ON DUPLICATE KEY UPDATE {{ .Ident "value" }} = {{ .Arg .Value }} +{{- else }} +ON CONFLICT ({{ .Ident "key_path" }}) DO UPDATE SET {{ .Ident "value" }} = {{ .Arg .Value }} +{{- end }} +; diff --git a/pkg/storage/unified/resource/data/sqlkv_update_datastore.sql b/pkg/storage/unified/resource/data/sqlkv_update_datastore.sql new file mode 100644 index 00000000000..677666e00a4 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_update_datastore.sql @@ -0,0 +1,3 @@ +UPDATE {{ .Ident .TableName }} +SET {{ .Ident "value" }} = {{ .Arg .Value }} +WHERE {{ .Ident "key_path" }} = {{ .Arg .KeyPath }}; diff --git a/pkg/storage/unified/resource/data/sqlkv_update_legacy_resource.sql b/pkg/storage/unified/resource/data/sqlkv_update_legacy_resource.sql new file mode 100644 index 00000000000..1565d0894a4 --- /dev/null +++ b/pkg/storage/unified/resource/data/sqlkv_update_legacy_resource.sql @@ -0,0 +1,9 @@ +UPDATE {{ .Ident "resource" }} +SET + {{ .Ident "value" }} = {{ .Arg .Value }}, + {{ .Ident "action" }} = {{ .Arg .Action }}, + {{ .Ident "folder" }} = {{ .Arg .Folder }} +WHERE {{ .Ident "group" }} = {{ .Arg .Group }} +AND {{ .Ident "resource" }} = {{ .Arg .Resource }} +AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +AND {{ .Ident "name" }} = {{ .Arg .Name }}; diff --git a/pkg/storage/unified/resource/datastore.go b/pkg/storage/unified/resource/datastore.go index 8a930492420..7a1b614323e 100644 --- a/pkg/storage/unified/resource/datastore.go +++ b/pkg/storage/unified/resource/datastore.go @@ -49,6 +49,9 @@ type DataKey struct { ResourceVersion int64 Action DataAction Folder string + + // needed to maintain backwards compatibility with unified/sql + GUID string } // GroupResource represents a unique group/resource combination @@ -61,6 +64,12 @@ func (k DataKey) String() string { return fmt.Sprintf("%s/%s/%s/%s/%d~%s~%s", k.Group, k.Resource, k.Namespace, k.Name, k.ResourceVersion, k.Action, k.Folder) } +// Temporary while we need to support unified/sql/backend compatibility +// Remove once we stop using RvManager in storage_backend.go +func (k DataKey) StringWithGUID() string { + return fmt.Sprintf("%s/%s/%s/%s/%d~%s~%s~%s", k.Group, k.Resource, k.Namespace, k.Name, k.ResourceVersion, k.Action, k.Folder, k.GUID) +} + func (k DataKey) Equals(other DataKey) bool { return k.Group == other.Group && k.Resource == other.Resource && k.Namespace == other.Namespace && k.Name == other.Name && k.ResourceVersion == other.ResourceVersion && k.Action == other.Action && k.Folder == other.Folder } @@ -516,7 +525,13 @@ func (d *dataStore) Save(ctx context.Context, key DataKey, value io.Reader) erro return fmt.Errorf("invalid data key: %w", err) } - writer, err := d.kv.Save(ctx, dataSection, key.String()) + var writer io.WriteCloser + var err error + if key.GUID != "" { + writer, err = d.kv.Save(ctx, dataSection, key.StringWithGUID()) + } else { + writer, err = d.kv.Save(ctx, dataSection, key.String()) + } if err != nil { return err } @@ -583,6 +598,33 @@ func ParseKey(key string) (DataKey, error) { }, nil } +// Temporary while we need to support unified/sql/backend compatibility +// Remove once we stop using RvManager in storage_backend.go +func ParseKeyWithGUID(key string) (DataKey, error) { + parts := strings.Split(key, "/") + if len(parts) != 5 { + return DataKey{}, fmt.Errorf("invalid key: %s", key) + } + rvActionFolderGUIDParts := strings.Split(parts[4], "~") + if len(rvActionFolderGUIDParts) != 4 { + return DataKey{}, fmt.Errorf("invalid key: %s", key) + } + rv, err := strconv.ParseInt(rvActionFolderGUIDParts[0], 10, 64) + if err != nil { + return DataKey{}, fmt.Errorf("invalid resource version '%s' in key %s: %w", rvActionFolderGUIDParts[0], key, err) + } + return DataKey{ + Group: parts[0], + Resource: parts[1], + Namespace: parts[2], + Name: parts[3], + ResourceVersion: rv, + Action: DataAction(rvActionFolderGUIDParts[1]), + Folder: rvActionFolderGUIDParts[2], + GUID: rvActionFolderGUIDParts[3], + }, nil +} + // SameResource checks if this key represents the same resource as another key. // It compares the identifying fields: Group, Resource, Namespace, and Name. // ResourceVersion, Action, and Folder are ignored as they don't identify the resource itself. diff --git a/pkg/storage/unified/resource/datastore_test.go b/pkg/storage/unified/resource/datastore_test.go index 8f167c2e16e..02c318fe6d9 100644 --- a/pkg/storage/unified/resource/datastore_test.go +++ b/pkg/storage/unified/resource/datastore_test.go @@ -9,6 +9,9 @@ import ( "testing" "github.com/bwmarrin/snowflake" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/stretchr/testify/require" ) @@ -24,6 +27,16 @@ func TestNewDataStore(t *testing.T) { require.NotNil(t, ds) } +// nolint:unused +func setupTestDataStoreSqlKv(t *testing.T) *dataStore { + dbstore := db.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + kv, err := NewSQLKV(eDB) + require.NoError(t, err) + return newDataStore(kv) +} + func TestDataKey_String(t *testing.T) { rv := int64(1934555792099250176) tests := []struct { @@ -679,10 +692,21 @@ func TestParseKey(t *testing.T) { } } -func TestDataStore_Save_And_Get(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() +func runDataStoreTestWith(t *testing.T, storeName string, newStoreFn func(*testing.T) *dataStore, testFn func(*testing.T, context.Context, *dataStore)) { + t.Run(storeName, func(t *testing.T) { + ctx := context.Background() + store := newStoreFn(t) + testFn(t, ctx, store) + }) +} +func TestDataStore_Save_And_Get(t *testing.T) { + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreSaveAndGet) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreSaveAndGet) +} + +func testDataStoreSaveAndGet(t *testing.T, ctx context.Context, ds *dataStore) { rv := node.Generate() testKey := DataKey{ @@ -744,9 +768,12 @@ func TestDataStore_Save_And_Get(t *testing.T) { } func TestDataStore_Delete(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreDelete) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreDelete) +} +func testDataStoreDelete(t *testing.T, ctx context.Context, ds *dataStore) { rv := node.Generate() testKey := DataKey{ @@ -795,9 +822,12 @@ func TestDataStore_Delete(t *testing.T) { } func TestDataStore_List(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreList) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreList) +} +func testDataStoreList(t *testing.T, ctx context.Context, ds *dataStore) { resourceKey := ListRequestKey{ Namespace: "test-namespace", Group: "test-group", @@ -919,9 +949,12 @@ func TestDataStore_List(t *testing.T) { } func TestDataStore_Integration(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreIntegration) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreIntegration) +} +func testDataStoreIntegration(t *testing.T, ctx context.Context, ds *dataStore) { t.Run("full lifecycle test", func(t *testing.T) { resourceKey := ListRequestKey{ Namespace: "integration-ns", @@ -1007,9 +1040,12 @@ func TestDataStore_Integration(t *testing.T) { } func TestDataStore_Keys(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreKeys) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreKeys) +} +func testDataStoreKeys(t *testing.T, ctx context.Context, ds *dataStore) { resourceKey := ListRequestKey{ Namespace: "test-namespace", Group: "test-group", @@ -1154,9 +1190,12 @@ func TestDataStore_Keys(t *testing.T) { } func TestDataStore_ValidationEnforced(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreValidationEnforced) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreValidationEnforced) +} +func testDataStoreValidationEnforced(t *testing.T, ctx context.Context, ds *dataStore) { // Create an invalid key invalidKey := DataKey{ Namespace: "Invalid-Namespace-$$$", @@ -1483,9 +1522,12 @@ func TestListRequestKey_Prefix(t *testing.T) { } func TestDataStore_LastResourceVersion(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreLastResourceVersion) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreLastResourceVersion) +} +func testDataStoreLastResourceVersion(t *testing.T, ctx context.Context, ds *dataStore) { t.Run("returns last resource version for existing data", func(t *testing.T) { resourceKey := ListRequestKey{ Namespace: "test-namespace", @@ -1585,9 +1627,12 @@ func TestDataStore_LastResourceVersion(t *testing.T) { } func TestDataStore_GetLatestResourceKey(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKey) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKey) +} +func testDataStoreGetLatestResourceKey(t *testing.T, ctx context.Context, ds *dataStore) { key := GetRequestKey{ Group: "apps", Resource: "resources", @@ -1648,9 +1693,12 @@ func TestDataStore_GetLatestResourceKey(t *testing.T) { } func TestDataStore_GetLatestResourceKey_Deleted(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKeyDeleted) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKeyDeleted) +} +func testDataStoreGetLatestResourceKeyDeleted(t *testing.T, ctx context.Context, ds *dataStore) { key := GetRequestKey{ Group: "apps", Resource: "resources", @@ -1676,9 +1724,12 @@ func TestDataStore_GetLatestResourceKey_Deleted(t *testing.T) { } func TestDataStore_GetLatestResourceKey_NotFound(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKeyNotFound) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKeyNotFound) +} +func testDataStoreGetLatestResourceKeyNotFound(t *testing.T, ctx context.Context, ds *dataStore) { key := GetRequestKey{ Group: "apps", Resource: "resources", @@ -1691,9 +1742,12 @@ func TestDataStore_GetLatestResourceKey_NotFound(t *testing.T) { } func TestDataStore_GetResourceKeyAtRevision(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetResourceKeyAtRevision) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetResourceKeyAtRevision) +} +func testDataStoreGetResourceKeyAtRevision(t *testing.T, ctx context.Context, ds *dataStore) { key := GetRequestKey{ Group: "apps", Resource: "resources", @@ -1766,9 +1820,12 @@ func TestDataStore_GetResourceKeyAtRevision(t *testing.T) { } func TestDataStore_ListLatestResourceKeys(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeys) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeys) +} +func testDataStoreListLatestResourceKeys(t *testing.T, ctx context.Context, ds *dataStore) { listKey := ListRequestKey{ Group: "apps", Resource: "resources", @@ -1819,9 +1876,12 @@ func TestDataStore_ListLatestResourceKeys(t *testing.T) { } func TestDataStore_ListLatestResourceKeys_Deleted(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeysDeleted) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeysDeleted) +} +func testDataStoreListLatestResourceKeysDeleted(t *testing.T, ctx context.Context, ds *dataStore) { listKey := ListRequestKey{ Group: "apps", Resource: "resources", @@ -1869,9 +1929,12 @@ func TestDataStore_ListLatestResourceKeys_Deleted(t *testing.T) { } func TestDataStore_ListLatestResourceKeys_Multiple(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeysMultiple) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeysMultiple) +} +func testDataStoreListLatestResourceKeysMultiple(t *testing.T, ctx context.Context, ds *dataStore) { listKey := ListRequestKey{ Group: "apps", Resource: "resources", @@ -1940,9 +2003,12 @@ func TestDataStore_ListLatestResourceKeys_Multiple(t *testing.T) { } func TestDataStore_ListResourceKeysAtRevision(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevision) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevision) +} +func testDataStoreListResourceKeysAtRevision(t *testing.T, ctx context.Context, ds *dataStore) { // Create multiple resources with different versions rv1 := node.Generate().Int64() rv2 := node.Generate().Int64() @@ -2152,9 +2218,12 @@ func TestDataStore_ListResourceKeysAtRevision(t *testing.T) { } func TestDataStore_ListResourceKeysAtRevision_ValidationErrors(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionValidationErrors) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionValidationErrors) +} +func testDataStoreListResourceKeysAtRevisionValidationErrors(t *testing.T, ctx context.Context, ds *dataStore) { tests := []struct { name string key ListRequestKey @@ -2194,9 +2263,12 @@ func TestDataStore_ListResourceKeysAtRevision_ValidationErrors(t *testing.T) { } func TestDataStore_ListResourceKeysAtRevision_EmptyResults(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionEmptyResults) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionEmptyResults) +} +func testDataStoreListResourceKeysAtRevisionEmptyResults(t *testing.T, ctx context.Context, ds *dataStore) { listKey := ListRequestKey{ Group: "apps", Resource: "resources", @@ -2213,9 +2285,12 @@ func TestDataStore_ListResourceKeysAtRevision_EmptyResults(t *testing.T) { } func TestDataStore_ListResourceKeysAtRevision_ResourcesNewerThanRevision(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision) +} +func testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision(t *testing.T, ctx context.Context, ds *dataStore) { // Create a resource with a high resource version rv := node.Generate().Int64() key := DataKey{ @@ -2681,9 +2756,12 @@ func TestGetRequestKey_Prefix(t *testing.T) { } func TestDataStore_GetResourceStats_Comprehensive(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetResourceStatsComprehensive) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetResourceStatsComprehensive) +} +func testDataStoreGetResourceStatsComprehensive(t *testing.T, ctx context.Context, ds *dataStore) { // Test setup: 3 namespaces × 3 groups × 3 resources × 3 names × 3 versions = 243 total entries // But each name will have only 1 latest version that counts, so 3 × 3 × 3 × 3 = 81 non-deleted resources namespaces := []string{"ns1", "ns2", "ns3"} @@ -2888,9 +2966,12 @@ func TestDataStore_GetResourceStats_Comprehensive(t *testing.T) { } func TestDataStore_getGroupResources(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetGroupResources) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetGroupResources) +} +func testDataStoreGetGroupResources(t *testing.T, ctx context.Context, ds *dataStore) { // Create test data with multiple group/resource combinations testData := []struct { group string @@ -2951,9 +3032,12 @@ func TestDataStore_getGroupResources(t *testing.T) { } func TestDataStore_BatchDelete(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreBatchDelete) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreBatchDelete) +} +func testDataStoreBatchDelete(t *testing.T, ctx context.Context, ds *dataStore) { keys := make([]DataKey, 95) for i := 0; i < 95; i++ { rv := node.Generate().Int64() @@ -2987,9 +3071,12 @@ func TestDataStore_BatchDelete(t *testing.T) { } func TestDataStore_BatchGet(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreBatchGet) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreBatchGet) +} +func testDataStoreBatchGet(t *testing.T, ctx context.Context, ds *dataStore) { t.Run("batch get multiple existing keys", func(t *testing.T) { // Create test data keys := make([]DataKey, 5) @@ -3132,9 +3219,12 @@ func TestDataStore_BatchGet(t *testing.T) { } func TestDataStore_GetLatestAndPredecessor(t *testing.T) { - ds := setupTestDataStore(t) - ctx := context.Background() + runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestAndPredecessor) + // enable this when sqlkv is ready + // runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestAndPredecessor) +} +func testDataStoreGetLatestAndPredecessor(t *testing.T, ctx context.Context, ds *dataStore) { resourceKey := ListRequestKey{ Namespace: "test-namespace", Group: "test-group", diff --git a/pkg/storage/unified/resource/eventstore.go b/pkg/storage/unified/resource/eventstore.go index 7f80fb6b87a..e0c01afb550 100644 --- a/pkg/storage/unified/resource/eventstore.go +++ b/pkg/storage/unified/resource/eventstore.go @@ -32,6 +32,7 @@ type EventKey struct { ResourceVersion int64 Action DataAction Folder string + GUID string } func (k EventKey) String() string { diff --git a/pkg/storage/unified/resource/eventstore_test.go b/pkg/storage/unified/resource/eventstore_test.go index a9d2ee93eb4..270db1ddd3f 100644 --- a/pkg/storage/unified/resource/eventstore_test.go +++ b/pkg/storage/unified/resource/eventstore_test.go @@ -7,6 +7,10 @@ import ( "time" "github.com/bwmarrin/snowflake" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,6 +25,20 @@ func setupTestEventStore(t *testing.T) *eventStore { return newEventStore(kv) } +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +// nolint:unused +func setupTestEventStoreSqlKv(t *testing.T) *eventStore { + dbstore := db.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + kv, err := NewSQLKV(eDB) + require.NoError(t, err) + return newEventStore(kv) +} + func TestNewEventStore(t *testing.T) { store := setupTestEventStore(t) assert.NotNil(t, store.kv) @@ -180,10 +198,21 @@ func TestEventStore_ParseEventKey(t *testing.T) { assert.Equal(t, originalKey, parsedKey) } -func TestEventStore_Save_Get(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) +func runEventStoreTestWith(t *testing.T, storeName string, newStoreFn func(*testing.T) *eventStore, testFn func(*testing.T, context.Context, *eventStore)) { + t.Run(storeName, func(t *testing.T) { + ctx := context.Background() + store := newStoreFn(t) + testFn(t, ctx, store) + }) +} +func TestEventStore_Save_Get(t *testing.T) { + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreSaveGet) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreSaveGet) +} + +func testEventStoreSaveGet(t *testing.T, ctx context.Context, store *eventStore) { event := Event{ Namespace: "default", Group: "apps", @@ -216,9 +245,12 @@ func TestEventStore_Save_Get(t *testing.T) { } func TestEventStore_Get_NotFound(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreGetNotFound) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreGetNotFound) +} +func testEventStoreGetNotFound(t *testing.T, ctx context.Context, store *eventStore) { nonExistentKey := EventKey{ Namespace: "default", Group: "apps", @@ -233,9 +265,12 @@ func TestEventStore_Get_NotFound(t *testing.T) { } func TestEventStore_LastEventKey(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreLastEventKey) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreLastEventKey) +} +func testEventStoreLastEventKey(t *testing.T, ctx context.Context, store *eventStore) { // Test when no events exist _, err := store.LastEventKey(ctx) assert.Error(t, err) @@ -292,9 +327,12 @@ func TestEventStore_LastEventKey(t *testing.T) { } func TestEventStore_ListKeysSince(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListKeysSince) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListKeysSince) +} +func testEventStoreListKeysSince(t *testing.T, ctx context.Context, store *eventStore) { // Add events with different resource versions events := []Event{ { @@ -349,9 +387,12 @@ func TestEventStore_ListKeysSince(t *testing.T) { } func TestEventStore_ListSince(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListSince) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListSince) +} +func testEventStoreListSince(t *testing.T, ctx context.Context, store *eventStore) { // Add events with different resource versions events := []Event{ { @@ -404,9 +445,12 @@ func TestEventStore_ListSince(t *testing.T) { } func TestEventStore_ListSince_Empty(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListSinceEmpty) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListSinceEmpty) +} +func testEventStoreListSinceEmpty(t *testing.T, ctx context.Context, store *eventStore) { // List events when store is empty retrievedEvents := make([]Event, 0) for event, err := range store.ListSince(ctx, 0) { @@ -459,9 +503,12 @@ func TestEventKey_Struct(t *testing.T) { } func TestEventStore_Save_InvalidJSON(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreSaveInvalidJSON) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreSaveInvalidJSON) +} +func testEventStoreSaveInvalidJSON(t *testing.T, ctx context.Context, store *eventStore) { // This should work fine as the Event struct should be serializable event := Event{ Namespace: "default", @@ -477,9 +524,12 @@ func TestEventStore_Save_InvalidJSON(t *testing.T) { } func TestEventStore_CleanupOldEvents(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEvents) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEvents) +} +func testEventStoreCleanupOldEvents(t *testing.T, ctx context.Context, store *eventStore) { now := time.Now() oldRV := snowflakeFromTime(now.Add(-48 * time.Hour)) // 48 hours ago recentRV := snowflakeFromTime(now.Add(-1 * time.Hour)) // 1 hour ago @@ -565,9 +615,12 @@ func TestEventStore_CleanupOldEvents(t *testing.T) { } func TestEventStore_CleanupOldEvents_NoOldEvents(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEventsNoOldEvents) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEventsNoOldEvents) +} +func testEventStoreCleanupOldEventsNoOldEvents(t *testing.T, ctx context.Context, store *eventStore) { // Create an event 1 hour old rv := snowflakeFromTime(time.Now().Add(-1 * time.Hour)) event := Event{ @@ -603,9 +656,12 @@ func TestEventStore_CleanupOldEvents_NoOldEvents(t *testing.T) { } func TestEventStore_CleanupOldEvents_EmptyStore(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEventsEmptyStore) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEventsEmptyStore) +} +func testEventStoreCleanupOldEventsEmptyStore(t *testing.T, ctx context.Context, store *eventStore) { // Clean up events from empty store deletedCount, err := store.CleanupOldEvents(ctx, time.Now().Add(-24*time.Hour)) require.NoError(t, err) @@ -613,9 +669,12 @@ func TestEventStore_CleanupOldEvents_EmptyStore(t *testing.T) { } func TestEventStore_BatchDelete(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreBatchDelete) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreBatchDelete) +} +func testEventStoreBatchDelete(t *testing.T, ctx context.Context, store *eventStore) { // Create multiple events (more than batch size to test batching) eventKeys := make([]string, 75) for i := 0; i < 75; i++ { @@ -722,9 +781,12 @@ func TestSnowflakeFromTime(t *testing.T) { } func TestListKeysSince_WithSnowflakeTime(t *testing.T) { - ctx := context.Background() - store := setupTestEventStore(t) + runEventStoreTestWith(t, "badger", setupTestEventStore, testListKeysSinceWithSnowflakeTime) + // enable this when sqlkv is ready + // runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testListKeysSinceWithSnowflakeTime) +} +func testListKeysSinceWithSnowflakeTime(t *testing.T, ctx context.Context, store *eventStore) { // Create events with snowflake-based resource versions at different times now := time.Now() events := []Event{ diff --git a/pkg/storage/unified/resource/fieldSelector.go b/pkg/storage/unified/resource/fieldSelector.go new file mode 100644 index 00000000000..034085ca07f --- /dev/null +++ b/pkg/storage/unified/resource/fieldSelector.go @@ -0,0 +1,53 @@ +package resource + +import ( + "context" + + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +// Some list queries can be calculated with simple reads or search index +func (s *server) tryFieldSelector(ctx context.Context, req *resourcepb.ListRequest) *resourcepb.ListResponse { + if req.Source != resourcepb.ListRequest_STORE || req.Options.Key.Namespace == "" { + return nil + } + + var names []string + for _, v := range req.Options.Fields { + if v.Key == "metadata.name" && v.Operator == `=` { + names = v.Values + continue + } + + // TODO: support other field selectors + } + + // The required names + if len(names) > 0 { + read := &resourcepb.ReadRequest{ + Key: req.Options.Key, + ResourceVersion: req.ResourceVersion, + } + rsp := &resourcepb.ListResponse{ + ResourceVersion: 1, // TODO, search result should include when it was indexed + } + for _, name := range names { + read.Key.Name = name + found, err := s.Read(ctx, read) + if err != nil { + return &resourcepb.ListResponse{Error: AsErrorResult(err)} + } + if len(found.Value) > 0 { + rsp.Items = append(rsp.Items, &resourcepb.ResourceWrapper{ + Value: found.Value, + ResourceVersion: found.ResourceVersion, + }) + if found.ResourceVersion > rsp.ResourceVersion { + rsp.ResourceVersion = found.ResourceVersion + } + } + } + return rsp + } + return nil +} diff --git a/pkg/storage/unified/resource/kv.go b/pkg/storage/unified/resource/kv.go index 043ea45f695..0bd413254a4 100644 --- a/pkg/storage/unified/resource/kv.go +++ b/pkg/storage/unified/resource/kv.go @@ -87,6 +87,9 @@ func (k *badgerKV) Get(ctx context.Context, section string, key string) (io.Read if section == "" { return nil, fmt.Errorf("section is required") } + if key == "" { + return nil, fmt.Errorf("key is required") + } key = section + "/" + key @@ -225,6 +228,9 @@ func (k *badgerKV) Delete(ctx context.Context, section string, key string) error if section == "" { return fmt.Errorf("section is required") } + if key == "" { + return fmt.Errorf("key is required") + } txn := k.db.NewTransaction(true) defer txn.Discard() diff --git a/pkg/storage/unified/resource/notifier_test.go b/pkg/storage/unified/resource/notifier_test.go index 7b201f47420..060f8eecfbe 100644 --- a/pkg/storage/unified/resource/notifier_test.go +++ b/pkg/storage/unified/resource/notifier_test.go @@ -6,6 +6,9 @@ import ( "time" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,6 +25,18 @@ func setupTestNotifier(t *testing.T) (*notifier, *eventStore) { return notifier, eventStore } +// nolint:unused +func setupTestNotifierSqlKv(t *testing.T) (*notifier, *eventStore) { + dbstore := db.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + kv, err := NewSQLKV(eDB) + require.NoError(t, err) + eventStore := newEventStore(kv) + notifier := newNotifier(eventStore, notifierOptions{log: &logging.NoOpLogger{}}) + return notifier, eventStore +} + func TestNewNotifier(t *testing.T) { notifier, _ := setupTestNotifier(t) @@ -35,10 +50,21 @@ func TestDefaultWatchOptions(t *testing.T) { assert.Equal(t, defaultBufferSize, opts.BufferSize) } -func TestNotifier_lastEventResourceVersion(t *testing.T) { - ctx := context.Background() - notifier, eventStore := setupTestNotifier(t) +func runNotifierTestWith(t *testing.T, storeName string, newStoreFn func(*testing.T) (*notifier, *eventStore), testFn func(*testing.T, context.Context, *notifier, *eventStore)) { + t.Run(storeName, func(t *testing.T) { + ctx := context.Background() + notifier, eventStore := newStoreFn(t) + testFn(t, ctx, notifier, eventStore) + }) +} +func TestNotifier_lastEventResourceVersion(t *testing.T) { + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierLastEventResourceVersion) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierLastEventResourceVersion) +} + +func testNotifierLastEventResourceVersion(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { // Test with no events rv, err := notifier.lastEventResourceVersion(ctx) assert.Error(t, err) @@ -85,8 +111,12 @@ func TestNotifier_lastEventResourceVersion(t *testing.T) { } func TestNotifier_cachekey(t *testing.T) { - notifier, _ := setupTestNotifier(t) + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierCachekey) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierCachekey) +} +func testNotifierCachekey(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { tests := []struct { name string event Event @@ -136,10 +166,14 @@ func TestNotifier_cachekey(t *testing.T) { } func TestNotifier_Watch_NoEvents(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchNoEvents) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchNoEvents) +} - notifier, eventStore := setupTestNotifier(t) +func testNotifierWatchNoEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { + ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + defer cancel() // Add at least one event so that lastEventResourceVersion doesn't return ErrNotFound initialEvent := Event{ @@ -174,10 +208,14 @@ func TestNotifier_Watch_NoEvents(t *testing.T) { } func TestNotifier_Watch_WithExistingEvents(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchWithExistingEvents) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchWithExistingEvents) +} - notifier, eventStore := setupTestNotifier(t) +func testNotifierWatchWithExistingEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() // Save some initial events initialEvents := []Event{ @@ -245,10 +283,14 @@ func TestNotifier_Watch_WithExistingEvents(t *testing.T) { } func TestNotifier_Watch_EventDeduplication(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchEventDeduplication) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchEventDeduplication) +} - notifier, eventStore := setupTestNotifier(t) +func testNotifierWatchEventDeduplication(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() // Add an initial event so that lastEventResourceVersion doesn't return ErrNotFound initialEvent := Event{ @@ -308,9 +350,13 @@ func TestNotifier_Watch_EventDeduplication(t *testing.T) { } func TestNotifier_Watch_ContextCancellation(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchContextCancellation) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchContextCancellation) +} - notifier, eventStore := setupTestNotifier(t) +func testNotifierWatchContextCancellation(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { + ctx, cancel := context.WithCancel(ctx) // Add an initial event so that lastEventResourceVersion doesn't return ErrNotFound initialEvent := Event{ @@ -351,10 +397,14 @@ func TestNotifier_Watch_ContextCancellation(t *testing.T) { } func TestNotifier_Watch_MultipleEvents(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() + runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchMultipleEvents) + // enable this when sqlkv is ready + // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchMultipleEvents) +} - notifier, eventStore := setupTestNotifier(t) +func testNotifierWatchMultipleEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() rv := time.Now().UnixNano() // Add an initial event so that lastEventResourceVersion doesn't return ErrNotFound initialEvent := Event{ diff --git a/pkg/storage/unified/resource/quotas.go b/pkg/storage/unified/resource/quotas.go index d956a6da017..03fe515559e 100644 --- a/pkg/storage/unified/resource/quotas.go +++ b/pkg/storage/unified/resource/quotas.go @@ -47,13 +47,14 @@ type Overrides struct { /* This service loads overrides (currently just quotas) from a YAML file with the following yaml structure: -"123": +overrides: + "123": quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 1500 + dashboard.grafana.app/dashboards: + limit: 1500 + folder.grafana.app/folders: + limit: 1500 */ func NewOverridesService(_ context.Context, logger log.Logger, reg prometheus.Registerer, tracer trace.Tracer, opts ReloadOptions) (*OverridesService, error) { // shouldn't be empty since we use file path existence to determine if we should enable the service @@ -76,12 +77,14 @@ func NewOverridesService(_ context.Context, logger log.Logger, reg prometheus.Re ReloadPeriod: opts.ReloadPeriod, LoadPath: []string{opts.FilePath}, Loader: func(r io.Reader) (interface{}, error) { - var tenants map[string]NamespaceOverrides + var raw struct { + Overrides map[string]NamespaceOverrides `yaml:"overrides"` + } decoder := yaml.NewDecoder(r) - if err := decoder.Decode(&tenants); err != nil { + if err := decoder.Decode(&raw); err != nil { return nil, err } - return &Overrides{Namespaces: tenants}, nil + return &Overrides{Namespaces: raw.Overrides}, nil }, } diff --git a/pkg/storage/unified/resource/quotas_test.go b/pkg/storage/unified/resource/quotas_test.go index 97f9f355af6..097a02d7444 100644 --- a/pkg/storage/unified/resource/quotas_test.go +++ b/pkg/storage/unified/resource/quotas_test.go @@ -27,10 +27,11 @@ func TestNewQuotaService(t *testing.T) { opts: ReloadOptions{}, setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -105,10 +106,11 @@ func TestQuotaService_ConfigReload(t *testing.T) { // Create a temporary config file tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - initialConfig := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + initialConfig := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(initialConfig), 0644)) @@ -139,14 +141,15 @@ func TestQuotaService_ConfigReload(t *testing.T) { assert.Equal(t, 1500, quota.Limit, "initial quota should be 1500") // Update the config file with new values - updatedConfig := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 2500 -"456": - quotas: - grafana.folder.app/folders: - limit: 3000 + updatedConfig := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 2500 + "456": + quotas: + grafana.folder.app/folders: + limit: 3000 ` require.NoError(t, os.WriteFile(tmpFile, []byte(updatedConfig), 0644)) @@ -183,10 +186,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns custom quota for matching tenant and resource", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -204,10 +208,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns default quota when tenant not found", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -225,10 +230,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns default quota when resource not found", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -246,10 +252,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "handles namespace without stacks- prefix", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -284,12 +291,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "handles multiple resources for same tenant", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -307,12 +315,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when namespace is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -330,12 +339,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when group is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -353,12 +363,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when resource is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -376,12 +387,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when all fields are empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index dd091c6e2d4..7c890e72b00 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -41,6 +41,7 @@ type ResourceServer interface { resourcepb.ManagedObjectIndexServer resourcepb.BlobStoreServer resourcepb.DiagnosticsServer + resourcepb.QuotasServer } type ListIterator interface { @@ -313,8 +314,7 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { } blobstore, err = NewCDKBlobSupport(ctx, CDKBlobSupportOptions{ - Tracer: tracer, - Bucket: NewInstrumentedBucket(bucket, opts.Reg, tracer), + Bucket: NewInstrumentedBucket(bucket, opts.Reg), }) if err != nil { return nil, err @@ -1039,7 +1039,7 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour } // Fast path for getting single value in a list - if rsp := s.tryFastPathList(ctx, req); rsp != nil { + if rsp := s.tryFieldSelector(ctx, req); rsp != nil { return rsp, nil } @@ -1084,10 +1084,6 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour return err } - item := &resourcepb.ResourceWrapper{ - ResourceVersion: iter.ResourceVersion(), - Value: iter.Value(), - } // Trash is only accessible to admins or the user who deleted the object if req.Source == resourcepb.ListRequest_TRASH { if !s.isTrashItemAuthorized(ctx, iter, trashChecker) { @@ -1097,6 +1093,11 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour continue } + item := &resourcepb.ResourceWrapper{ + ResourceVersion: iter.ResourceVersion(), + Value: iter.Value(), + } + pageBytes += len(item.Value) rsp.Items = append(rsp.Items, item) if (req.Limit > 0 && len(rsp.Items) >= int(req.Limit)) || pageBytes >= maxPageBytes { @@ -1136,40 +1137,6 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour return rsp, err } -// Some list queries can be calculated with simple reads -func (s *server) tryFastPathList(ctx context.Context, req *resourcepb.ListRequest) *resourcepb.ListResponse { - if req.Source != resourcepb.ListRequest_STORE || req.Options.Key.Namespace == "" { - return nil - } - - for _, v := range req.Options.Fields { - if v.Key == "metadata.name" && v.Operator == `=` { - if len(v.Values) == 1 { - read := &resourcepb.ReadRequest{ - Key: req.Options.Key, - ResourceVersion: req.ResourceVersion, - } - read.Key.Name = v.Values[0] - found, err := s.Read(ctx, read) - if err != nil { - return &resourcepb.ListResponse{Error: AsErrorResult(err)} - } - - // Return a value when it exists - rsp := &resourcepb.ListResponse{} - if len(found.Value) > 0 { - rsp.Items = []*resourcepb.ResourceWrapper{{ - Value: found.Value, - ResourceVersion: found.ResourceVersion, - }} - } - return rsp - } - } - } - return nil -} - // isTrashItemAuthorized checks if the user has access to the trash item. func (s *server) isTrashItemAuthorized(ctx context.Context, iter ListIterator, trashChecker claims.ItemChecker) bool { user, ok := claims.AuthInfoFrom(ctx) @@ -1466,6 +1433,38 @@ func (s *server) PutBlob(ctx context.Context, req *resourcepb.PutBlobRequest) (* return rsp, nil } +func (s *server) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest) (*resourcepb.QuotaUsageResponse, error) { + if s.overridesService == nil { + return &resourcepb.QuotaUsageResponse{Error: &resourcepb.ErrorResult{ + Message: "overrides service not configured on resource server", + Code: http.StatusNotImplemented, + }}, nil + } + nsr := NamespacedResource{ + Namespace: req.Key.Namespace, + Group: req.Key.Group, + Resource: req.Key.Resource, + } + usage, err := s.backend.GetResourceStats(ctx, nsr, 0) + if err != nil { + return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil + } + limit, err := s.overridesService.GetQuota(ctx, nsr) + if err != nil { + return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil + } + + // handle case where no resources exist yet - very unlikely but possible + rsp := &resourcepb.QuotaUsageResponse{Limit: int64(limit.Limit)} + if len(usage) <= 0 { + rsp.Usage = 0 + } else { + rsp.Usage = usage[0].Count + } + + return rsp, nil +} + func (s *server) getPartialObject(ctx context.Context, key *resourcepb.ResourceKey, rv int64) (utils.GrafanaMetaAccessor, *resourcepb.ErrorResult) { if r := verifyRequestKey(key); r != nil { return nil, r @@ -1585,6 +1584,7 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { )) if s.overridesService == nil { + s.log.FromContext(ctx).Debug("overrides service not configured, skipping quota check", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) return } @@ -1599,6 +1599,12 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { s.log.FromContext(ctx).Error("failed to get resource stats for quota checking", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err) return } + if len(stats) > 0 { + s.log.FromContext(ctx).Debug("stats found", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "count", stats[0].Count) + } else { + s.log.FromContext(ctx).Debug("no stats found for resource", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) + } + if len(stats) > 0 && stats[0].Count >= int64(quota.Limit) { s.log.FromContext(ctx).Info("Quota exceeded on create", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "quota", quota.Limit, "count", stats[0].Count, "stats_resource", stats[0].Resource) } diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 504c49616f9..0b0bbcfba13 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "net/http" + "os" + "path/filepath" "strings" "sync" "testing" @@ -22,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/scheduler" ) @@ -614,3 +617,71 @@ func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) { check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil) check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil) } + +func TestGetQuotaUsage(t *testing.T) { + ctx := context.Background() + + t.Run("returns error when overrides service is not configured", func(t *testing.T) { + s := &server{ + overridesService: nil, + log: log.NewNopLogger(), + } + + resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: "stacks-123", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }) + require.NoError(t, err) + require.NotNil(t, resp.Error) + assert.Equal(t, int32(http.StatusNotImplemented), resp.Error.Code) + assert.Equal(t, "overrides service not configured on resource server", resp.Error.Message) + }) + + t.Run("returns usage and limit successfully", func(t *testing.T) { + // Create a temporary overrides config file + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `overrides: + "123": + quotas: + dashboard.grafana.app/dashboards: + limit: 500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + + // Create a real OverridesService with the temp file + overridesService, err := NewOverridesService(ctx, log.NewNopLogger(), prometheus.NewRegistry(), tracing.NewNoopTracerService(), ReloadOptions{ + FilePath: tmpFile, + }) + require.NoError(t, err) + require.NoError(t, overridesService.init(ctx)) + defer func() { + _ = overridesService.stop(ctx) + }() + + // Create a mock backend that returns resource stats (reusing mockStorageBackend from search_test.go) + mockBackend := &mockStorageBackend{ + resourceStats: []ResourceStats{{Count: 42}}, + } + + s := &server{ + backend: mockBackend, + overridesService: overridesService, + log: log.NewNopLogger(), + } + + resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: "stacks-123", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }) + require.NoError(t, err) + require.Nil(t, resp.Error) + assert.Equal(t, int64(42), resp.Usage) + assert.Equal(t, int64(500), resp.Limit) + }) +} diff --git a/pkg/storage/unified/resource/sqlkv.go b/pkg/storage/unified/resource/sqlkv.go new file mode 100644 index 00000000000..6d406294a96 --- /dev/null +++ b/pkg/storage/unified/resource/sqlkv.go @@ -0,0 +1,568 @@ +package resource + +import ( + "bytes" + "context" + "database/sql" + "embed" + "errors" + "fmt" + "io" + "iter" + "strings" + "text/template" + "time" + + "github.com/google/uuid" + "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +// Templates setup. +var ( + //go:embed data/*.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`)) +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +// Templates. +var ( + sqlKVKeys = mustTemplate("sqlkv_keys.sql") + sqlKVGet = mustTemplate("sqlkv_get.sql") + sqlKVBatchGet = mustTemplate("sqlkv_batch_get.sql") + sqlKVSaveEvent = mustTemplate("sqlkv_save_event.sql") + sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql") + sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql") + sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql") + sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql") + sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql") + sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql") + sqlKVDelete = mustTemplate("sqlkv_delete.sql") + sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql") +) + +// sqlKVSection can be embedded in structs used when rendering query templates +// for queries that reference a particular section. The section will be validated, +// and the template can directly reference the `TableName`. +type sqlKVSection struct { + Section string +} + +func (req sqlKVSection) Validate() error { + if req.Section == "" { + return fmt.Errorf("section is required") + } + + if req.Section != dataSection && req.Section != eventsSection { + return fmt.Errorf("invalid section: %s", req.Section) + } + + return nil +} + +func (req sqlKVSection) TableName() string { + if req.Section == dataSection { + return "resource_history" + } + + return "resource_events" +} + +// sqlKVSectionKey can be embedded in structs used when rendering query templates +// for queries that reference both a section and a particular key. The `key` is +// validated, and the template can reference the corresponding `KeyPath`. +type sqlKVSectionKey struct { + sqlKVSection + Key string +} + +func (req sqlKVSectionKey) Validate() error { + if err := req.sqlKVSection.Validate(); err != nil { + return err + } + if req.Key == "" { + return fmt.Errorf("key is required") + } + + return nil +} + +func (req sqlKVSectionKey) KeyPath() string { + return req.Section + "/" + req.Key +} + +type sqlKVGetRequest struct { + sqltemplate.SQLTemplate + sqlKVSectionKey + *sqlKVGetResponse +} + +type sqlKVGetResponse struct { + Value []byte +} + +func (req sqlKVGetRequest) Validate() error { + return req.sqlKVSectionKey.Validate() +} + +func (req sqlKVGetRequest) Results() ([]byte, error) { + return req.Value, nil +} + +type sqlKVBatchRequest struct { + sqltemplate.SQLTemplate + sqlKVSection + Keys []string +} + +func (req sqlKVBatchRequest) Validate() error { + return req.sqlKVSection.Validate() +} + +func (req sqlKVBatchRequest) KeyPaths() []string { + result := make([]string, 0, len(req.Keys)) + for _, key := range req.Keys { + result = append(result, req.Section+"/"+key) + } + + return result +} + +type sqlKVSaveRequest struct { + sqltemplate.SQLTemplate + sqlKVSectionKey + Value []byte + + // old fields that can be removed once we prune resource_history + GUID string + Group string + Resource string + Namespace string + Name string + Action int64 + Folder string +} + +func (req sqlKVSaveRequest) Validate() error { + return req.sqlKVSectionKey.Validate() +} + +type sqlKVLegacySaveRequest struct { + sqltemplate.SQLTemplate + Value []byte + GUID string + Group string + Resource string + Namespace string + Name string + Action int64 + Folder string +} + +func (req sqlKVLegacySaveRequest) Validate() error { + return nil +} + +func (req sqlKVLegacySaveRequest) Results() ([]byte, error) { + return req.Value, nil +} + +type sqlKVKeysRequest struct { + sqltemplate.SQLTemplate + sqlKVSection + Options ListOptions +} + +func (req sqlKVKeysRequest) Validate() error { + return req.sqlKVSection.Validate() +} + +func (req sqlKVKeysRequest) StartKey() string { + return req.Section + "/" + req.Options.StartKey +} + +func (req sqlKVKeysRequest) EndKey() string { + if req.Options.EndKey == "" { + req.Options.EndKey = PrefixRangeEnd(req.Section + "/") + } + + return req.Section + "/" + req.Options.EndKey +} + +func (req sqlKVKeysRequest) SortAscending() bool { + return req.Options.Sort != SortOrderDesc +} + +type sqlKVDeleteRequest struct { + sqltemplate.SQLTemplate + sqlKVSectionKey +} + +func (req sqlKVDeleteRequest) Validate() error { + return req.sqlKVSectionKey.Validate() +} + +var _ KV = &sqlKV{} + +type sqlKV struct { + dbProvider db.DBProvider + db db.DB + dialect sqltemplate.Dialect +} + +func NewSQLKV(dbProvider db.DBProvider) (KV, error) { + if dbProvider == nil { + return nil, fmt.Errorf("dbProvider is required") + } + + ctx := context.Background() + dbConn, err := dbProvider.Init(ctx) + if err != nil { + return nil, fmt.Errorf("error initializing DB: %w", err) + } + + dialect := sqltemplate.DialectForDriver(dbConn.DriverName()) + if dialect == nil { + return nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName()) + } + + return &sqlKV{ + dbProvider: dbProvider, + db: dbConn, + dialect: dialect, + }, nil +} + +func (k *sqlKV) Ping(ctx context.Context) error { + return k.db.PingContext(ctx) +} + +func (k *sqlKV) Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] { + return func(yield func(string, error) bool) { + rows, err := dbutil.QueryRows(ctx, k.db, sqlKVKeys, sqlKVKeysRequest{ + SQLTemplate: sqltemplate.New(k.dialect), + sqlKVSection: sqlKVSection{section}, + Options: opt, + }) + if err != nil { + yield("", err) + return + } + defer closeRows(rows, yield) + + for rows.Next() { + var key string + if err := rows.Scan(&key); err != nil { + yield("", fmt.Errorf("error reading row: %w", err)) + return + } + + if !yield(strings.TrimPrefix(key, section+"/"), nil) { + return + } + } + + if err := rows.Err(); err != nil { + yield("", fmt.Errorf("failed to read rows: %w", err)) + } + } +} + +func (k *sqlKV) Get(ctx context.Context, section string, key string) (io.ReadCloser, error) { + value, err := dbutil.QueryRow(ctx, k.db, sqlKVGet, sqlKVGetRequest{ + SQLTemplate: sqltemplate.New(k.dialect), + sqlKVSectionKey: sqlKVSectionKey{sqlKVSection{section}, key}, + sqlKVGetResponse: new(sqlKVGetResponse), + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("failed to get key: %w", err) + } + + return io.NopCloser(bytes.NewReader(value)), nil +} + +func (k *sqlKV) BatchGet(ctx context.Context, section string, keys []string) iter.Seq2[KeyValue, error] { + return func(yield func(KeyValue, error) bool) { + if len(keys) == 0 { + return + } + + rows, err := dbutil.QueryRows(ctx, k.db, sqlKVBatchGet, sqlKVBatchRequest{ + SQLTemplate: sqltemplate.New(k.dialect), + sqlKVSection: sqlKVSection{section}, + Keys: keys, + }) + if err != nil { + yield(KeyValue{}, err) + return + } + defer closeRows(rows, yield) + + for rows.Next() { + var key string + var value []byte + if err := rows.Scan(&key, &value); err != nil { + yield(KeyValue{}, fmt.Errorf("error reading row: %w", err)) + return + } + + kv := KeyValue{ + Key: strings.TrimPrefix(key, section+"/"), + Value: io.NopCloser(bytes.NewReader(value)), + } + if !yield(kv, nil) { + return + } + } + + if err := rows.Err(); err != nil { + yield(KeyValue{}, fmt.Errorf("failed to read rows: %w", err)) + } + } +} + +func (k *sqlKV) Save(ctx context.Context, section string, key string) (io.WriteCloser, error) { + sectionKey := sqlKVSectionKey{sqlKVSection{section}, key} + if err := sectionKey.Validate(); err != nil { + return nil, err + } + + return &sqlWriteCloser{ + kv: k, + ctx: ctx, + sectionKey: sectionKey, + buf: &bytes.Buffer{}, + closed: false, + }, nil +} + +type sqlWriteCloser struct { + kv *sqlKV + ctx context.Context + sectionKey sqlKVSectionKey + buf *bytes.Buffer + closed bool +} + +func (w *sqlWriteCloser) Write(value []byte) (int, error) { + if w.closed { + return 0, errors.New("write to closed writer") + } + + return w.buf.Write(value) +} + +func (w *sqlWriteCloser) Close() error { + if w.closed { + return nil + } + + w.closed = true + + // do regular kv save: simple key_path + value insert with conflict check. + // can only do this on resource_events for now, until we drop the columns in resource_history + if w.sectionKey.Section == eventsSection { + _, err := dbutil.Exec(w.ctx, w.kv.db, sqlKVSaveEvent, sqlKVSaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + sqlKVSectionKey: w.sectionKey, + Value: w.buf.Bytes(), + }) + + if err != nil { + return fmt.Errorf("failed to save: %w", err) + } + + return nil + } + + // if storage_backend is running with an RvManager, it will inject a transaction into the context + // used to keep backwards compatibility between sql-based kvstore and unified/sql/backend + tx, ok := rvmanager.TxFromCtx(w.ctx) + if !ok { + // temporary save for dataStore without rvmanager + // we can use the same template as the event one after we: + // - move PK from GUID to key_path + // - remove all unnecessary columns (or at least their NOT NULL constraints) + _, err := w.kv.Get(w.ctx, w.sectionKey.Section, w.sectionKey.Key) + if errors.Is(err, ErrNotFound) { + _, err := dbutil.Exec(w.ctx, w.kv.db, sqlKVInsertData, sqlKVSaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + sqlKVSectionKey: w.sectionKey, + GUID: uuid.New().String(), + Value: w.buf.Bytes(), + }) + + if err != nil { + return fmt.Errorf("failed to insert to datastore: %w", err) + } + + return nil + } + + if err != nil { + return fmt.Errorf("failed to get for save: %w", err) + } + + _, err = dbutil.Exec(w.ctx, w.kv.db, sqlKVUpdateData, sqlKVSaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + sqlKVSectionKey: w.sectionKey, + Value: w.buf.Bytes(), + }) + + if err != nil { + return fmt.Errorf("failed to update to datastore: %w", err) + } + + return nil + } + + // special, temporary save that includes all the fields in resource_history that are not relevant for the kvstore, + // as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that + // component will be responsible for populating the resource_version and key_path columns + // note that we are not touching resource_version table, neither the resource_version columns or the key_path column + // as the RvManager will be responsible for this + dataKey, err := ParseKeyWithGUID(w.sectionKey.Key) + if err != nil { + return fmt.Errorf("failed to parse key: %w", err) + } + + var action int64 + switch dataKey.Action { + case DataActionCreated: + action = 1 + case DataActionUpdated: + action = 2 + case DataActionDeleted: + action = 3 + default: + return fmt.Errorf("failed to parse key: %w", err) + } + + _, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + sqlKVSectionKey: w.sectionKey, + Value: w.buf.Bytes(), + GUID: dataKey.GUID, + Group: dataKey.Group, + Resource: dataKey.Resource, + Namespace: dataKey.Namespace, + Name: dataKey.Name, + Action: action, + Folder: dataKey.Folder, + }) + + if err != nil { + return fmt.Errorf("failed to save to resource_history: %w", err) + } + + switch dataKey.Action { + case DataActionCreated: + _, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + Value: w.buf.Bytes(), + GUID: dataKey.GUID, + Group: dataKey.Group, + Resource: dataKey.Resource, + Namespace: dataKey.Namespace, + Name: dataKey.Name, + Action: action, + Folder: dataKey.Folder, + }) + + if err != nil { + return fmt.Errorf("failed to insert to resource: %w", err) + } + case DataActionUpdated: + _, err = dbutil.Exec(w.ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + Value: w.buf.Bytes(), + Group: dataKey.Group, + Resource: dataKey.Resource, + Namespace: dataKey.Namespace, + Name: dataKey.Name, + Action: action, + Folder: dataKey.Folder, + }) + + if err != nil { + return fmt.Errorf("failed to update resource: %w", err) + } + case DataActionDeleted: + _, err = dbutil.Exec(w.ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{ + SQLTemplate: sqltemplate.New(w.kv.dialect), + Group: dataKey.Group, + Resource: dataKey.Resource, + Namespace: dataKey.Namespace, + Name: dataKey.Name, + }) + + if err != nil { + return fmt.Errorf("failed to delete from resource: %w", err) + } + } + + return nil +} + +func (k *sqlKV) Delete(ctx context.Context, section string, key string) error { + res, err := dbutil.Exec(ctx, k.db, sqlKVDelete, sqlKVDeleteRequest{ + SQLTemplate: sqltemplate.New(k.dialect), + sqlKVSectionKey: sqlKVSectionKey{sqlKVSection{section}, key}, + }) + if err != nil { + return fmt.Errorf("failed to delete key: %w", err) + } + + rows, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("failed to validate delete: %w", err) + } + + if rows == 0 { + return ErrNotFound + } + + // TODO reflect change to resource table + + return nil +} + +func (k *sqlKV) BatchDelete(ctx context.Context, section string, keys []string) error { + if len(keys) == 0 { + return nil + } + + if _, err := dbutil.Exec(ctx, k.db, sqlKVBatchDelete, sqlKVBatchRequest{ + SQLTemplate: sqltemplate.New(k.dialect), + sqlKVSection: sqlKVSection{section}, + Keys: keys, + }); err != nil { + return fmt.Errorf("failed to batch delete keys: %w", err) + } + + return nil +} + +func (k *sqlKV) UnixTimestamp(ctx context.Context) (int64, error) { + return time.Now().Unix(), nil +} + +func closeRows[T any](rows db.Rows, yield func(T, error) bool) { + if err := rows.Close(); err != nil { + var zero T + yield(zero, fmt.Errorf("error closing rows: %w", err)) + } +} diff --git a/pkg/storage/unified/resource/storage_backend.go b/pkg/storage/unified/resource/storage_backend.go index 0de97b0355e..13f2b9d6159 100644 --- a/pkg/storage/unified/resource/storage_backend.go +++ b/pkg/storage/unified/resource/storage_backend.go @@ -14,6 +14,7 @@ import ( "time" "github.com/bwmarrin/snowflake" + "github.com/google/uuid" "github.com/grafana/grafana-app-sdk/logging" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" @@ -22,6 +23,8 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" "github.com/grafana/grafana/pkg/util/debouncer" ) @@ -68,9 +71,16 @@ type kvStorageBackend struct { withExperimentalClusterScope bool //tracer trace.Tracer //reg prometheus.Registerer + + rvManager *rvmanager.ResourceVersionManager } -var _ StorageBackend = &kvStorageBackend{} +var _ KVBackend = &kvStorageBackend{} + +type KVBackend interface { + StorageBackend + resourcepb.DiagnosticsServer +} type KVBackendOptions struct { KvStore KV @@ -80,9 +90,13 @@ type KVBackendOptions struct { 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 + + // Adding RvManager overrides the RV generated with snowflake in order to keep backwards compatibility with + // unified/sql + RvManager *rvmanager.ResourceVersionManager } -func NewKVStorageBackend(opts KVBackendOptions) (StorageBackend, error) { +func NewKVStorageBackend(opts KVBackendOptions) (KVBackend, error) { ctx := context.Background() kv := opts.KvStore @@ -114,6 +128,7 @@ func NewKVStorageBackend(opts KVBackendOptions) (StorageBackend, error) { eventRetentionPeriod: eventRetentionPeriod, eventPruningInterval: eventPruningInterval, withExperimentalClusterScope: opts.WithExperimentalClusterScope, + rvManager: opts.RvManager, } err = backend.initPruner(ctx) if err != nil { @@ -126,6 +141,18 @@ func NewKVStorageBackend(opts KVBackendOptions) (StorageBackend, error) { return backend, nil } +func (k *kvStorageBackend) IsHealthy(ctx context.Context, _ *resourcepb.HealthCheckRequest) (*resourcepb.HealthCheckResponse, error) { + type pinger interface { + Ping(context.Context) error + } + if p, ok := k.kv.(pinger); ok { + if err := p.Ping(ctx); err != nil { + return &resourcepb.HealthCheckResponse{Status: resourcepb.HealthCheckResponse_NOT_SERVING}, fmt.Errorf("KV store health check failed: %w", err) + } + } + return &resourcepb.HealthCheckResponse{Status: resourcepb.HealthCheckResponse_SERVING}, nil +} + // runCleanupOldEvents starts a background goroutine that periodically cleans up old events func (k *kvStorageBackend) runCleanupOldEvents(ctx context.Context) { // Run cleanup every hour @@ -300,9 +327,28 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Action: action, Folder: obj.GetFolder(), } - err := k.dataStore.Save(ctx, dataKey, bytes.NewReader(event.Value)) - if err != nil { - return 0, fmt.Errorf("failed to write data: %w", err) + + if k.rvManager != nil { + dataKey.GUID = uuid.New().String() + var err error + rv, err = k.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) { + err := k.dataStore.Save(rvmanager.ContextWithTx(ctx, tx), dataKey, bytes.NewReader(event.Value)) + if err != nil { + return "", fmt.Errorf("failed to write data: %w", err) + } + + return dataKey.GUID, nil + }) + if err != nil { + return 0, fmt.Errorf("failed to write data: %w", err) + } + + dataKey.ResourceVersion = rv + } else { + err := k.dataStore.Save(ctx, dataKey, bytes.NewReader(event.Value)) + if err != nil { + return 0, fmt.Errorf("failed to write data: %w", err) + } } // Optimistic concurrency control to verify our write is the latest version @@ -323,14 +369,22 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } // Check if the RV we just wrote is the latest. If not, a concurrent write with higher RV happened - if latestKey.ResourceVersion != rv { + if !rvmanager.IsRvEqual(latestKey.ResourceVersion, rv) { // Delete the data we just wrote since it's not the latest + // if we're running with rvManager, convert the ResourceVersion back to snowflake to delete + if k.rvManager != nil { + dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(dataKey.ResourceVersion) + } _ = k.dataStore.Delete(ctx, dataKey) return 0, fmt.Errorf("optimistic locking failed: concurrent modification detected") } - if prevKey.ResourceVersion != event.PreviousRV { + if !rvmanager.IsRvEqual(prevKey.ResourceVersion, event.PreviousRV) { // Another concurrent write happened between our read and write + // if we're running with rvManager, convert the ResourceVersion back to snowflake to delete + if k.rvManager != nil { + dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(dataKey.ResourceVersion) + } _ = k.dataStore.Delete(ctx, dataKey) return 0, fmt.Errorf("optimistic locking failed: resource was modified concurrently (expected previous RV %d, found %d)", event.PreviousRV, prevKey.ResourceVersion) } @@ -349,8 +403,12 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } // Check if the RV we just wrote is the latest. If not, a concurrent create with higher RV happened - if latestKey.ResourceVersion != rv { + if !rvmanager.IsRvEqual(latestKey.ResourceVersion, rv) { // Delete the data we just wrote since it's not the latest + // if we're running with rvManager, convert the ResourceVersion back to snowflake to delete + if k.rvManager != nil { + dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(dataKey.ResourceVersion) + } _ = k.dataStore.Delete(ctx, dataKey) return 0, fmt.Errorf("optimistic locking failed: concurrent create detected") } @@ -358,6 +416,10 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in // Verify that the immediate predecessor is not a create if prevKey.Action == DataActionCreated { // Another concurrent create happened - delete our write and return error + // if we're running with rvManager, convert the ResourceVersion back to snowflake to delete + if k.rvManager != nil { + dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(dataKey.ResourceVersion) + } _ = k.dataStore.Delete(ctx, dataKey) return 0, fmt.Errorf("optimistic locking failed: concurrent create detected") } @@ -374,7 +436,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Folder: obj.GetFolder(), PreviousRV: event.PreviousRV, } - err = k.eventStore.Save(ctx, eventData) + err := k.eventStore.Save(ctx, eventData) if err != nil { // Clean up the data we wrote since event save failed _ = k.dataStore.Delete(ctx, dataKey) diff --git a/pkg/storage/unified/resourcepb/resource.pb.go b/pkg/storage/unified/resourcepb/resource.pb.go index 8c6fb47b491..8046b354ccd 100644 --- a/pkg/storage/unified/resourcepb/resource.pb.go +++ b/pkg/storage/unified/resourcepb/resource.pb.go @@ -2484,6 +2484,114 @@ func (x *ResourceTableRow) GetObject() []byte { return nil } +type QuotaUsageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Namespace (tenant) + Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuotaUsageRequest) Reset() { + *x = QuotaUsageRequest{} + mi := &file_resource_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuotaUsageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuotaUsageRequest) ProtoMessage() {} + +func (x *QuotaUsageRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_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 QuotaUsageRequest.ProtoReflect.Descriptor instead. +func (*QuotaUsageRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{30} +} + +func (x *QuotaUsageRequest) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +type QuotaUsageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Error details + Error *ErrorResult `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + // Current usage + Usage int64 `protobuf:"varint,2,opt,name=usage,proto3" json:"usage,omitempty"` + // Current limit + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuotaUsageResponse) Reset() { + *x = QuotaUsageResponse{} + mi := &file_resource_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuotaUsageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuotaUsageResponse) ProtoMessage() {} + +func (x *QuotaUsageResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_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 QuotaUsageResponse.ProtoReflect.Descriptor instead. +func (*QuotaUsageResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{31} +} + +func (x *QuotaUsageResponse) GetError() *ErrorResult { + if x != nil { + return x.Error + } + return nil +} + +func (x *QuotaUsageResponse) GetUsage() int64 { + if x != nil { + return x.Usage + } + return 0 +} + +func (x *QuotaUsageResponse) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + type WatchEvent_Resource struct { state protoimpl.MessageState `protogen:"open.v1"` Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` @@ -2494,7 +2602,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2506,7 +2614,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2553,7 +2661,7 @@ type BulkResponse_Summary struct { func (x *BulkResponse_Summary) Reset() { *x = BulkResponse_Summary{} - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2565,7 +2673,7 @@ func (x *BulkResponse_Summary) String() string { func (*BulkResponse_Summary) ProtoMessage() {} func (x *BulkResponse_Summary) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2649,7 +2757,7 @@ type BulkResponse_Rejected struct { func (x *BulkResponse_Rejected) Reset() { *x = BulkResponse_Rejected{} - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2661,7 +2769,7 @@ func (x *BulkResponse_Rejected) String() string { func (*BulkResponse_Rejected) ProtoMessage() {} func (x *BulkResponse_Rejected) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2718,7 +2826,7 @@ type ListManagedObjectsResponse_Item struct { func (x *ListManagedObjectsResponse_Item) Reset() { *x = ListManagedObjectsResponse_Item{} - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2730,7 +2838,7 @@ func (x *ListManagedObjectsResponse_Item) String() string { func (*ListManagedObjectsResponse_Item) ProtoMessage() {} func (x *ListManagedObjectsResponse_Item) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2801,7 +2909,7 @@ type CountManagedObjectsResponse_ResourceCount struct { func (x *CountManagedObjectsResponse_ResourceCount) Reset() { *x = CountManagedObjectsResponse_ResourceCount{} - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +2921,7 @@ func (x *CountManagedObjectsResponse_ResourceCount) String() string { func (*CountManagedObjectsResponse_ResourceCount) ProtoMessage() {} func (x *CountManagedObjectsResponse_ResourceCount) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2886,7 +2994,7 @@ type ResourceTableColumnDefinition_Properties struct { func (x *ResourceTableColumnDefinition_Properties) Reset() { *x = ResourceTableColumnDefinition_Properties{} - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2898,7 +3006,7 @@ func (x *ResourceTableColumnDefinition_Properties) String() string { func (*ResourceTableColumnDefinition_Properties) ProtoMessage() {} func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3320,68 +3428,85 @@ var file_resource_proto_rawDesc = string([]byte{ 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2a, 0x49, - 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, - 0x41, 0x54, 0x45, 0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, - 0x6e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, - 0x44, 0x5f, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x56, 0x32, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x09, 0x0a, 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, - 0x78, 0x61, 0x63, 0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, - 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, - 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, - 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, - 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, - 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, - 0x63, 0x65, 0x73, 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, - 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, - 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, - 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x3c, + 0x0a, 0x11, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x6d, 0x0a, 0x12, + 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x14, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2a, 0x49, 0x0a, 0x14, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, + 0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, + 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x45, + 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, + 0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, + 0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, + 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x74, 0x6f, + 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, + 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, + 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x54, 0x0a, 0x06, 0x51, 0x75, 0x6f, 0x74, 0x61, + 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, + 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x61, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, + 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, + 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, }) var ( @@ -3397,7 +3522,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (ResourceVersionMatchV2)(0), // 1: resource.ResourceVersionMatchV2 @@ -3436,12 +3561,14 @@ var file_resource_proto_goTypes = []any{ (*ResourceTable)(nil), // 34: resource.ResourceTable (*ResourceTableColumnDefinition)(nil), // 35: resource.ResourceTableColumnDefinition (*ResourceTableRow)(nil), // 36: resource.ResourceTableRow - (*WatchEvent_Resource)(nil), // 37: resource.WatchEvent.Resource - (*BulkResponse_Summary)(nil), // 38: resource.BulkResponse.Summary - (*BulkResponse_Rejected)(nil), // 39: resource.BulkResponse.Rejected - (*ListManagedObjectsResponse_Item)(nil), // 40: resource.ListManagedObjectsResponse.Item - (*CountManagedObjectsResponse_ResourceCount)(nil), // 41: resource.CountManagedObjectsResponse.ResourceCount - (*ResourceTableColumnDefinition_Properties)(nil), // 42: resource.ResourceTableColumnDefinition.Properties + (*QuotaUsageRequest)(nil), // 37: resource.QuotaUsageRequest + (*QuotaUsageResponse)(nil), // 38: resource.QuotaUsageResponse + (*WatchEvent_Resource)(nil), // 39: resource.WatchEvent.Resource + (*BulkResponse_Summary)(nil), // 40: resource.BulkResponse.Summary + (*BulkResponse_Rejected)(nil), // 41: resource.BulkResponse.Rejected + (*ListManagedObjectsResponse_Item)(nil), // 42: resource.ListManagedObjectsResponse.Item + (*CountManagedObjectsResponse_ResourceCount)(nil), // 43: resource.CountManagedObjectsResponse.ResourceCount + (*ResourceTableColumnDefinition_Properties)(nil), // 44: resource.ResourceTableColumnDefinition.Properties } var file_resource_proto_depIdxs = []int32{ 10, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails @@ -3465,51 +3592,55 @@ var file_resource_proto_depIdxs = []int32{ 9, // 18: resource.ListResponse.error:type_name -> resource.ErrorResult 21, // 19: resource.WatchRequest.options:type_name -> resource.ListOptions 3, // 20: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 37, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 37, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 39, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 39, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource 7, // 23: resource.BulkRequest.key:type_name -> resource.ResourceKey 4, // 24: resource.BulkRequest.action:type_name -> resource.BulkRequest.Action 9, // 25: resource.BulkResponse.error:type_name -> resource.ErrorResult - 38, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary - 39, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected - 40, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item + 40, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary + 41, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected + 42, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item 9, // 29: resource.ListManagedObjectsResponse.error:type_name -> resource.ErrorResult - 41, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount + 43, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount 9, // 31: resource.CountManagedObjectsResponse.error:type_name -> resource.ErrorResult 5, // 32: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus 35, // 33: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition 36, // 34: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow 6, // 35: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType - 42, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties + 44, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties 7, // 37: resource.ResourceTableRow.key:type_name -> resource.ResourceKey - 7, // 38: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey - 4, // 39: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action - 7, // 40: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey - 18, // 41: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 12, // 42: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 14, // 43: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 16, // 44: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 22, // 45: resource.ResourceStore.List:input_type -> resource.ListRequest - 24, // 46: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 26, // 47: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest - 30, // 48: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest - 28, // 49: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest - 32, // 50: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 19, // 51: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 13, // 52: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 15, // 53: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 17, // 54: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 23, // 55: resource.ResourceStore.List:output_type -> resource.ListResponse - 25, // 56: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 27, // 57: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse - 31, // 58: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse - 29, // 59: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse - 33, // 60: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 51, // [51:61] is the sub-list for method output_type - 41, // [41:51] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 7, // 38: resource.QuotaUsageRequest.key:type_name -> resource.ResourceKey + 9, // 39: resource.QuotaUsageResponse.error:type_name -> resource.ErrorResult + 7, // 40: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey + 4, // 41: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action + 7, // 42: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey + 18, // 43: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 12, // 44: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 14, // 45: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 16, // 46: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 22, // 47: resource.ResourceStore.List:input_type -> resource.ListRequest + 24, // 48: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 26, // 49: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest + 30, // 50: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest + 28, // 51: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest + 32, // 52: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 37, // 53: resource.Quotas.GetQuotaUsage:input_type -> resource.QuotaUsageRequest + 19, // 54: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 13, // 55: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 15, // 56: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 17, // 57: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 23, // 58: resource.ResourceStore.List:output_type -> resource.ListResponse + 25, // 59: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 27, // 60: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse + 31, // 61: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse + 29, // 62: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse + 33, // 63: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 38, // 64: resource.Quotas.GetQuotaUsage:output_type -> resource.QuotaUsageResponse + 54, // [54:65] is the sub-list for method output_type + 43, // [43:54] is the sub-list for method input_type + 43, // [43:43] is the sub-list for extension type_name + 43, // [43:43] is the sub-list for extension extendee + 0, // [0:43] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -3524,9 +3655,9 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)), NumEnums: 7, - NumMessages: 36, + NumMessages: 38, NumExtensions: 0, - NumServices: 4, + NumServices: 5, }, GoTypes: file_resource_proto_goTypes, DependencyIndexes: file_resource_proto_depIdxs, diff --git a/pkg/storage/unified/resourcepb/resource_grpc.pb.go b/pkg/storage/unified/resourcepb/resource_grpc.pb.go index c90aa0f26a6..a37ab264d76 100644 --- a/pkg/storage/unified/resourcepb/resource_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/resource_grpc.pb.go @@ -709,3 +709,94 @@ var Diagnostics_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "resource.proto", } + +const ( + Quotas_GetQuotaUsage_FullMethodName = "/resource.Quotas/GetQuotaUsage" +) + +// QuotasClient is the client API for Quotas service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type QuotasClient interface { + // Get current quota usage and limits + GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error) +} + +type quotasClient struct { + cc grpc.ClientConnInterface +} + +func NewQuotasClient(cc grpc.ClientConnInterface) QuotasClient { + return "asClient{cc} +} + +func (c *quotasClient) GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QuotaUsageResponse) + err := c.cc.Invoke(ctx, Quotas_GetQuotaUsage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QuotasServer is the server API for Quotas service. +// All implementations should embed UnimplementedQuotasServer +// for forward compatibility +type QuotasServer interface { + // Get current quota usage and limits + GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error) +} + +// UnimplementedQuotasServer should be embedded to have forward compatible implementations. +type UnimplementedQuotasServer struct { +} + +func (UnimplementedQuotasServer) GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetQuotaUsage not implemented") +} + +// UnsafeQuotasServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to QuotasServer will +// result in compilation errors. +type UnsafeQuotasServer interface { + mustEmbedUnimplementedQuotasServer() +} + +func RegisterQuotasServer(s grpc.ServiceRegistrar, srv QuotasServer) { + s.RegisterService(&Quotas_ServiceDesc, srv) +} + +func _Quotas_GetQuotaUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuotaUsageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QuotasServer).GetQuotaUsage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quotas_GetQuotaUsage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QuotasServer).GetQuotaUsage(ctx, req.(*QuotaUsageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Quotas_ServiceDesc is the grpc.ServiceDesc for Quotas service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Quotas_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.Quotas", + HandlerType: (*QuotasServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetQuotaUsage", + Handler: _Quotas_GetQuotaUsage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "resource.proto", +} diff --git a/pkg/storage/unified/resourcepb/search.pb.go b/pkg/storage/unified/resourcepb/search.pb.go index 91f5aadfa97..459e9aa3429 100644 --- a/pkg/storage/unified/resourcepb/search.pb.go +++ b/pkg/storage/unified/resourcepb/search.pb.go @@ -388,7 +388,7 @@ func (x *ResourceSearchResponse) GetFacet() map[string]*ResourceSearchResponse_F type RebuildIndexesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Namespace (tenant) + // Namespace (tenant) must be the same as all keys' namespace Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` // List of ResourceKeys (Namespace + Group + Resource) Keys []*ResourceKey `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index a8c1efb431c..d6ff00a81c0 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -25,8 +25,9 @@ import ( bleveSearch "github.com/blevesearch/bleve/v2/search/searcher" index "github.com/blevesearch/bleve_index_api" "github.com/prometheus/client_golang/prometheus" + bolterrors "go.etcd.io/bbolt/errors" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "go.uber.org/atomic" "k8s.io/apimachinery/pkg/selection" @@ -42,11 +43,9 @@ import ( ) const ( - // tracingPrexfixBleve is the prefix used for tracing spans in the Bleve backend - tracingPrexfixBleve = "unified_search.bleve." - indexStorageMemory = "memory" indexStorageFile = "file" + boltTimeout = "500ms" ) // Keys used to store internal data in index. @@ -55,6 +54,8 @@ const ( internalBuildInfoKey = "build_info" // Encoded as JSON of buildInfo struct ) +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/search") + var _ resource.SearchBackend = &bleveBackend{} var _ resource.ResourceIndex = &bleveIndex{} @@ -83,9 +84,8 @@ type BleveOptions struct { } type bleveBackend struct { - tracer trace.Tracer - log log.Logger - opts BleveOptions + log log.Logger + opts BleveOptions // set from opts.OwnsIndex, always non-nil ownsIndexFn func(key resource.NamespacedResource) (bool, error) @@ -99,7 +99,7 @@ type bleveBackend struct { bgTasksWg sync.WaitGroup } -func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resource.BleveIndexMetrics) (*bleveBackend, error) { +func NewBleveBackend(opts BleveOptions, indexMetrics *resource.BleveIndexMetrics) (*bleveBackend, error) { if opts.Root == "" { return nil, fmt.Errorf("bleve backend missing root folder configuration") } @@ -138,7 +138,6 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resou be := &bleveBackend{ log: l, - tracer: tracer, cache: map[resource.NamespacedResource]*bleveIndex{}, opts: opts, ownsIndexFn: ownFn, @@ -358,7 +357,7 @@ func (b *bleveBackend) BuildIndex( updater resource.UpdateFn, rebuild bool, ) (resource.ResourceIndex, error) { - _, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex") + _, span := tracer.Start(ctx, "search.bleveBackend.BuildIndex") defer span.End() span.SetAttributes( @@ -418,14 +417,25 @@ func (b *bleveBackend) BuildIndex( // This happens on startup, or when memory-based index has expired. (We don't expire file-based indexes) // If we do have an unexpired cached index already, we always build a new index from scratch. if cachedIndex == nil && !rebuild { - index, fileIndexName, indexRV = b.findPreviousFileBasedIndex(resourceDir) + result := b.findPreviousFileBasedIndex(resourceDir) + if result != nil && result.IsOpen { + // Index file exists but is opened by another process, fallback to memory. + // Keep the name so we can skip cleanup of that directory. + newIndexType = indexStorageMemory + fileIndexName = result.Name + } else if result != nil && result.Index != nil { + // Found and opened existing index successfully + index = result.Index + fileIndexName = result.Name + indexRV = result.RV + } } - if index != nil { + if newIndexType == indexStorageFile && index != nil { build = false logWithDetails.Debug("Existing index found on filesystem", "indexRV", indexRV, "directory", filepath.Join(resourceDir, fileIndexName)) defer closeIndexOnExit(index, "") // Close index, but don't delete directory. - } else { + } else if newIndexType == indexStorageFile { // Building index from scratch. Index name has a time component in it to be unique, but if // we happen to create non-unique name, we bump the time and try again. @@ -452,7 +462,9 @@ func (b *bleveBackend) BuildIndex( logWithDetails.Info("Building index using filesystem", "directory", indexDir) defer closeIndexOnExit(index, indexDir) // Close index, and delete new index directory. } - } else { + } + + if newIndexType == indexStorageMemory { index, err = newBleveIndex("", mapper, time.Now(), b.opts.BuildVersion) if err != nil { return nil, fmt.Errorf("error creating new in-memory bleve index: %w", err) @@ -555,30 +567,30 @@ func cleanFileSegment(input string) string { return input } -// cleanOldIndexes deletes all subdirectories inside dir, skipping directory with "skipName". +// cleanOldIndexes deletes all subdirectories inside resourceDir, skipping directory with "skipName". // "skipName" can be empty. -func (b *bleveBackend) cleanOldIndexes(dir string, skipName string) { - files, err := os.ReadDir(dir) +func (b *bleveBackend) cleanOldIndexes(resourceDir string, skipName string) { + entries, err := os.ReadDir(resourceDir) if err != nil { if os.IsNotExist(err) { return } - b.log.Warn("error cleaning folders from", "directory", dir, "error", err) + b.log.Warn("error cleaning folders from", "directory", resourceDir, "error", err) return } - for _, file := range files { - if file.IsDir() && file.Name() != skipName { - fpath := filepath.Join(dir, file.Name()) - if !isPathWithinRoot(fpath, b.opts.Root) { - b.log.Warn("Skipping cleanup of directory", "directory", fpath) + for _, ent := range entries { + if ent.IsDir() && ent.Name() != skipName { + indexDir := filepath.Join(resourceDir, ent.Name()) + if !isPathWithinRoot(indexDir, b.opts.Root) { + b.log.Warn("Skipping cleanup of directory", "directory", indexDir) continue } - err = os.RemoveAll(fpath) + err = os.RemoveAll(indexDir) if err != nil { - b.log.Error("Unable to remove old index folder", "directory", fpath, "error", err) + b.log.Error("Unable to remove old index folder", "directory", indexDir, "error", err) } else { - b.log.Info("Removed old index folder", "directory", fpath) + b.log.Info("Removed old index folder", "directory", indexDir) } } } @@ -625,10 +637,17 @@ func formatIndexName(now time.Time) string { return now.Format("20060102-150405") } -func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string) (bleve.Index, string, int64) { +type fileIndex struct { + Index bleve.Index + Name string + RV int64 + IsOpen bool +} + +func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string) *fileIndex { entries, err := os.ReadDir(resourceDir) if err != nil { - return nil, "", 0 + return nil } for _, ent := range entries { @@ -638,8 +657,13 @@ func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string) (bleve.Ind indexName := ent.Name() indexDir := filepath.Join(resourceDir, indexName) - idx, err := bleve.Open(indexDir) + + idx, err := bleve.OpenUsing(indexDir, map[string]interface{}{"bolt_timeout": boltTimeout}) if err != nil { + if errors.Is(err, bolterrors.ErrTimeout) { + b.log.Debug("Index is opened by another process (timeout), skipping", "indexDir", indexDir) + return &fileIndex{Name: indexName, IsOpen: true} + } b.log.Debug("error opening index", "indexDir", indexDir, "err", err) continue } @@ -651,10 +675,14 @@ func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string) (bleve.Ind continue } - return idx, indexName, indexRV + return &fileIndex{ + Index: idx, + Name: indexName, + RV: indexRV, + } } - return nil, "", 0 + return nil } // Stop closes all indexes and stops background tasks. @@ -713,7 +741,6 @@ type bleveIndex struct { // The values returned with all allFields []*resourcepb.ResourceTableColumnDefinition - tracing trace.Tracer logger log.Logger updaterFn resource.UpdateFn @@ -750,7 +777,6 @@ func (b *bleveBackend) newBleveIndex( fields: fields, allFields: allFields, standard: standardSearchFields, - tracing: b.tracer, logger: logger, updaterFn: updaterFn, minUpdateInterval: b.opts.IndexMinUpdateInterval, @@ -1018,7 +1044,7 @@ func (b *bleveIndex) Search( federate []resource.ResourceIndex, // For federated queries, these will match the values in req.federate stats *resource.SearchStats, ) (*resourcepb.ResourceSearchResponse, error) { - ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"Search") + ctx, span := tracer.Start(ctx, "search.bleveIndex.Search") defer span.End() if req.Options == nil || req.Options.Key == nil { @@ -1098,7 +1124,7 @@ func (b *bleveIndex) Search( } func (b *bleveIndex) DocCount(ctx context.Context, folder string, stats *resource.SearchStats) (int64, error) { - ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"DocCount") + ctx, span := tracer.Start(ctx, "search.bleveIndex.DocCount") defer span.End() if folder == "" { @@ -1144,7 +1170,7 @@ func (b *bleveIndex) getIndex( req *resourcepb.ResourceSearchRequest, federate []resource.ResourceIndex, ) (bleve.Index, error) { - _, span := b.tracing.Start(ctx, tracingPrexfixBleve+"getIndex") + _, span := tracer.Start(ctx, "search.bleveIndex.getIndex") defer span.End() if len(req.Federated) != len(federate) { @@ -1171,7 +1197,7 @@ func (b *bleveIndex) getIndex( } func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resourcepb.ErrorResult) { - ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"toBleveSearchRequest") + ctx, span := tracer.Start(ctx, "search.bleveIndex.toBleveSearchRequest") defer span.End() facets := bleve.FacetsRequest{} @@ -1493,7 +1519,7 @@ func (b *bleveIndex) runUpdater(ctx context.Context) { } func (b *bleveIndex) updateIndexWithLatestModifications(ctx context.Context, requests int) (int64, error) { - ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"updateIndexWithLatestModifications") + ctx, span := tracer.Start(ctx, "search.bleveIndex.updateIndexWithLatestModifications") defer span.End() sinceRV := b.resourceVersion @@ -1691,7 +1717,7 @@ func filterValue(field string, v string) string { } func (b *bleveIndex) hitsToTable(ctx context.Context, selectFields []string, hits search.DocumentMatchCollection, explain bool) (*resourcepb.ResourceTable, error) { - _, span := b.tracing.Start(ctx, tracingPrexfixBleve+"hitsToTable") + _, span := tracer.Start(ctx, "search.bleveIndex.hitsToTable") defer span.End() fields := []*resourcepb.ResourceTableColumnDefinition{} diff --git a/pkg/storage/unified/search/bleve_integration_test.go b/pkg/storage/unified/search/bleve_integration_test.go index 9ee16a1b559..819fd5a8d9a 100644 --- a/pkg/storage/unified/search/bleve_integration_test.go +++ b/pkg/storage/unified/search/bleve_integration_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/unified/resource" unitest "github.com/grafana/grafana/pkg/storage/unified/testing" ) @@ -20,7 +19,7 @@ func TestBleveSearchBackend(t *testing.T) { backend, err := NewBleveBackend(BleveOptions{ Root: tempDir, FileThreshold: 5, - }, tracing.NewNoopTracerService(), nil) + }, nil) require.NoError(t, err) require.NotNil(t, backend) @@ -45,7 +44,7 @@ func TestSearchBackendBenchmark(t *testing.T) { // Create a new bleve backend backend, err := NewBleveBackend(BleveOptions{ Root: tempDir, - }, tracing.NewNoopTracerService(), nil) + }, nil) require.NoError(t, err) require.NotNil(t, backend) diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index 7374374fd25..c10aa3f6726 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -259,7 +258,7 @@ func newTestDashboardsIndex(t testing.TB, threshold int64, size int64, writer re backend, err := search.NewBleveBackend(search.BleveOptions{ Root: t.TempDir(), FileThreshold: threshold, // use in-memory for tests - }, tracing.NewNoopTracerService(), nil) + }, nil) require.NoError(t, err) t.Cleanup(backend.Stop) diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 2d2e7142eaf..c879440e7b6 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/user" @@ -51,7 +50,7 @@ func TestBleveBackend(t *testing.T) { backend, err := NewBleveBackend(BleveOptions{ Root: tmpdir, FileThreshold: 5, // with more than 5 items we create a file on disk - }, tracing.NewNoopTracerService(), nil) + }, nil) require.NoError(t, err) t.Cleanup(backend.Stop) @@ -782,7 +781,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro opts.Root = t.TempDir() } - backend, err := NewBleveBackend(opts, tracing.NewNoopTracerService(), metrics) + backend, err := NewBleveBackend(opts, metrics) require.NoError(t, err) require.NotNil(t, backend) t.Cleanup(backend.Stop) @@ -1558,7 +1557,7 @@ func TestInvalidBuildVersion(t *testing.T) { Root: t.TempDir(), BuildVersion: "invalid", } - _, err := NewBleveBackend(opts, tracing.NewNoopTracerService(), nil) + _, err := NewBleveBackend(opts, nil) require.ErrorContains(t, err, "cannot parse build version") } @@ -1584,3 +1583,76 @@ func docCount(t *testing.T, idx resource.ResourceIndex) int { require.NoError(t, err) return int(cnt) } + +func TestBleveBackendFallsBackToMemory(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + // First, create a file-based index with one backend and keep it open + backend1, reg1 := setupBleveBackend(t, withRootDir(tmpDir)) + index1, err := backend1.BuildIndex(context.Background(), ns, 100 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), nil, false) + require.NoError(t, err) + require.NotNil(t, index1) + + // Verify first index is file-based + bleveIdx1, ok := index1.(*bleveIndex) + require.True(t, ok) + require.Equal(t, indexStorageFile, bleveIdx1.indexStorage) + checkOpenIndexes(t, reg1, 0, 1) + + // Now create a second backend using the same directory + // This simulates another instance trying to open the same index + backend2, reg2 := setupBleveBackend(t, withRootDir(tmpDir)) + + // BuildIndex should detect the file is locked and fallback to memory + index2, err := backend2.BuildIndex(context.Background(), ns, 100 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), nil, false) + require.NoError(t, err) + require.NotNil(t, index2) + + // Verify second index fell back to in-memory despite size being above file threshold + bleveIdx2, ok := index2.(*bleveIndex) + require.True(t, ok) + require.Equal(t, indexStorageMemory, bleveIdx2.indexStorage) + + // Verify metrics show 1 memory index and 0 file indexes for backend2 + checkOpenIndexes(t, reg2, 1, 0) + + // Verify the in-memory index works correctly + require.Equal(t, 10, docCount(t, index2)) + + // Clean up: close first backend to release the file lock + backend1.Stop() +} + +func TestBleveSkipCleanOldIndexesOnMemoryFallback(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1, _ := setupBleveBackend(t, withRootDir(tmpDir)) + _, err := backend1.BuildIndex(context.Background(), ns, 100 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), nil, false) + require.NoError(t, err) + + // Now create a second backend using the same directory + // This simulates another instance trying to open the same index + backend2, _ := setupBleveBackend(t, withRootDir(tmpDir)) + + // BuildIndex should detect the file is locked and fallback to memory + _, err = backend2.BuildIndex(context.Background(), ns, 100 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), nil, false) + require.NoError(t, err) + + // Verify that the index directory still exists (i.e., cleanOldIndexes was skipped) + verifyDirEntriesCount(t, backend2.getResourceDir(ns), 1) + + // Clean up: close first backend to release the file lock + backend1.Stop() +} diff --git a/pkg/storage/unified/search/builders/document_test.go b/pkg/storage/unified/search/builders/document_test.go index 6c8d02d6cb8..5fcae177696 100644 --- a/pkg/storage/unified/search/builders/document_test.go +++ b/pkg/storage/unified/search/builders/document_test.go @@ -53,8 +53,9 @@ func TestUserDocumentBuilder(t *testing.T) { Group: "iam.grafana.app", Resource: "users", }, []string{ - "user-with-login-and-email", - "user-with-login-only", + "with-login-and-email", + "with-login-only", + "with-last-seen-at-and-role", }) } diff --git a/pkg/storage/unified/search/builders/team_search.go b/pkg/storage/unified/search/builders/team_search.go index 4b09074b3dc..3dcf3cfdc95 100644 --- a/pkg/storage/unified/search/builders/team_search.go +++ b/pkg/storage/unified/search/builders/team_search.go @@ -46,8 +46,8 @@ func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) { return resource.DocumentBuilderInfo{ GroupResource: schema.GroupResource{ - Group: "iam.grafana.app", - Resource: "searchTeams", + Group: v0alpha1.TeamResourceInfo.GroupResource().Group, + Resource: v0alpha1.TeamResourceInfo.GroupResource().Resource, }, Fields: fields, Builder: new(teamSearchBuilder), diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role-out.json new file mode 100644 index 00000000000..84e06fd5b90 --- /dev/null +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role-out.json @@ -0,0 +1,16 @@ +{ + "key": { + "namespace": "default", + "group": "iam.grafana.app", + "resource": "users", + "name": "with-last-seen-at-and-role" + }, + "name": "with-last-seen-at-and-role", + "rv": 1234, + "fields": { + "email": "user.three@test.com", + "lastSeenAt": 1698321600, + "login": "user.three", + "role": "Editor" + } +} diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role.json new file mode 100644 index 00000000000..fdd964240a6 --- /dev/null +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-last-seen-at-and-role.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "with-last-seen-at-and-role", + "namespace": "default" + }, + "spec": { + "login": "user.three", + "email": "user.three@test.com", + "role": "Editor" + }, + "status": { + "lastSeenAt": 1698321600 + } +} diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email-out.json similarity index 55% rename from pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email-out.json rename to pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email-out.json index b7f75247149..04fd081d011 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email-out.json +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email-out.json @@ -3,12 +3,14 @@ "namespace": "default", "group": "iam.grafana.app", "resource": "users", - "name": "user-with-login-and-email" + "name": "with-login-and-email" }, - "name": "user-with-login-and-email", + "name": "with-login-and-email", "rv": 1234, "fields": { "email": "user.one@test.com", - "login": "user.one" + "lastSeenAt": 0, + "login": "user.one", + "role": "Viewer" } } \ No newline at end of file diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email.json similarity index 79% rename from pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email.json rename to pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email.json index a7eebbc754a..894bf6e977b 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email.json +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-and-email.json @@ -1,6 +1,6 @@ { "metadata": { - "name": "user-with-login-and-email", + "name": "with-login-and-email", "namespace": "default" }, "spec": { diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-only-out.json similarity index 51% rename from pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only-out.json rename to pkg/storage/unified/search/builders/testdata/doc/user-with-login-only-out.json index 7672d4b2a09..3aeaeec020e 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only-out.json +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-only-out.json @@ -3,11 +3,13 @@ "namespace": "default", "group": "iam.grafana.app", "resource": "users", - "name": "user-with-login-only" + "name": "with-login-only" }, - "name": "user-with-login-only", + "name": "with-login-only", "rv": 1234, "fields": { - "login": "user.two" + "lastSeenAt": 0, + "login": "user.two", + "role": "Viewer" } } \ No newline at end of file diff --git a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only.json b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-only.json similarity index 77% rename from pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only.json rename to pkg/storage/unified/search/builders/testdata/doc/user-with-login-only.json index 6b3adf2dc1d..affbe8a9c5e 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only.json +++ b/pkg/storage/unified/search/builders/testdata/doc/user-with-login-only.json @@ -1,6 +1,6 @@ { "metadata": { - "name": "user-with-login-only", + "name": "with-login-only", "namespace": "default" }, "spec": { diff --git a/pkg/storage/unified/search/builders/user.go b/pkg/storage/unified/search/builders/user.go index b17cdb99b15..c18c9d33cdc 100644 --- a/pkg/storage/unified/search/builders/user.go +++ b/pkg/storage/unified/search/builders/user.go @@ -12,10 +12,20 @@ import ( ) const ( - USER_EMAIL = "email" - USER_LOGIN = "login" + USER_EMAIL = "email" + USER_LOGIN = "login" + USER_LAST_SEEN_AT = "lastSeenAt" + USER_ROLE = "role" ) +// UserSortableExtraFields are the additional fields that can be used for sorting user search results. +// Should not include standard fields like title. +var UserSortableExtraFields = []string{ + USER_EMAIL, + USER_LOGIN, + USER_LAST_SEEN_AT, +} + var UserTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{ USER_EMAIL: { Name: USER_EMAIL, @@ -35,6 +45,22 @@ var UserTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefin Filterable: true, }, }, + USER_LAST_SEEN_AT: { + Name: USER_LAST_SEEN_AT, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + Description: "The last seen timestamp of the user", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: true, + }, + }, + USER_ROLE: { + Name: USER_ROLE, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + Description: "The role of the user", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: true, + }, + }, } func GetUserBuilder() (resource.DocumentBuilderInfo, error) { @@ -75,6 +101,8 @@ func (u *userDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb if user.Spec.Login != "" { doc.Fields[USER_LOGIN] = user.Spec.Login } + doc.Fields[USER_LAST_SEEN_AT] = user.Status.LastSeenAt + doc.Fields[USER_ROLE] = user.Spec.Role return doc, nil } diff --git a/pkg/storage/unified/search/options.go b/pkg/storage/unified/search/options.go index 0550a32ead4..20a0874b598 100644 --- a/pkg/storage/unified/search/options.go +++ b/pkg/storage/unified/search/options.go @@ -5,7 +5,6 @@ import ( "path/filepath" "github.com/Masterminds/semver" - "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" @@ -15,7 +14,6 @@ import ( func NewSearchOptions( features featuremgmt.FeatureToggles, cfg *setting.Cfg, - tracer trace.Tracer, docs resource.DocumentBuilderSupplier, indexMetrics *resource.BleveIndexMetrics, ownsIndexFn func(key resource.NamespacedResource) (bool, error), @@ -48,7 +46,7 @@ func NewSearchOptions( BuildVersion: cfg.BuildVersion, OwnsIndex: ownsIndexFn, IndexMinUpdateInterval: cfg.IndexMinUpdateInterval, - }, tracer, indexMetrics) + }, indexMetrics) if err != nil { return resource.SearchOptions{}, err diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 2416609761d..a129a01727a 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -14,9 +14,9 @@ import ( "github.com/jackc/pgx/v5/pgconn" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" "go.uber.org/atomic" "google.golang.org/protobuf/proto" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -31,11 +31,13 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/grafana/grafana/pkg/util/debouncer" ) -const tracePrefix = "sql.resource." +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/sql") + const defaultPollingInterval = 100 * time.Millisecond const defaultWatchBufferSize = 100 // number of events to buffer in the watch stream const defaultPrunerHistoryLimit = 20 @@ -56,7 +58,6 @@ type Backend interface { type BackendOptions struct { DBProvider db.DBProvider - Tracer trace.Tracer Reg prometheus.Registerer PollingInterval time.Duration WatchBufferSize int @@ -74,9 +75,6 @@ func NewBackend(opts BackendOptions) (Backend, error) { if opts.DBProvider == nil { return nil, errors.New("no db provider") } - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("sql-backend") - } ctx, cancel := context.WithCancel(context.Background()) if opts.PollingInterval == 0 { @@ -90,7 +88,6 @@ func NewBackend(opts BackendOptions) (Backend, error) { done: ctx.Done(), cancel: cancel, log: logging.DefaultLogger.With("logger", "sql-resource-server"), - tracer: opts.Tracer, reg: opts.Reg, dbProvider: opts.DBProvider, pollingInterval: opts.PollingInterval, @@ -114,7 +111,6 @@ type backend struct { // o11y log logging.Logger - tracer trace.Tracer reg prometheus.Registerer storageMetrics *resource.StorageMetrics @@ -131,7 +127,7 @@ type backend struct { notifier eventNotifier // resource version manager - rvManager *resourceVersionManager + rvManager *rvmanager.ResourceVersionManager // testing simulatedNetworkLatency time.Duration @@ -168,10 +164,9 @@ func (b *backend) initLocked(ctx context.Context) error { } // Initialize ResourceVersionManager - rvManager, err := NewResourceVersionManager(ResourceManagerOptions{ + rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{ Dialect: b.dialect, DB: b.db, - Tracer: b.tracer, }) if err != nil { return fmt.Errorf("failed to create resource version manager: %w", err) @@ -264,7 +259,7 @@ func (b *backend) Stop(_ context.Context) error { // GetResourceStats implements Backend. func (b *backend) GetResourceStats(ctx context.Context, nsr resource.NamespacedResource, minCount int) ([]resource.ResourceStats, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"GetResourceStats", trace.WithAttributes( + ctx, span := tracer.Start(ctx, "sql.backend.GetResourceStats", trace.WithAttributes( attribute.String("namespace", nsr.Namespace), attribute.String("group", nsr.Group), attribute.String("resource", nsr.Resource), @@ -304,7 +299,7 @@ func (b *backend) GetResourceStats(ctx context.Context, nsr resource.NamespacedR } func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (int64, error) { - _, span := b.tracer.Start(ctx, tracePrefix+"WriteEvent") + _, span := tracer.Start(ctx, "sql.backend.WriteEvent") defer span.End() // TODO: validate key ? switch event.Type { @@ -320,7 +315,7 @@ func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (in } func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"Create") + ctx, span := tracer.Start(ctx, "sql.backend.create") defer span.End() folder := "" @@ -408,7 +403,7 @@ func IsRowAlreadyExistsError(err error) bool { } func (b *backend) update(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"Update") + ctx, span := tracer.Start(ctx, "sql.backend.update") defer span.End() folder := "" @@ -468,7 +463,7 @@ func (b *backend) update(ctx context.Context, event resource.WriteEvent) (int64, } func (b *backend) delete(ctx context.Context, event resource.WriteEvent) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"Delete") + ctx, span := tracer.Start(ctx, "sql.backend.delete") defer span.End() folder := "" @@ -547,7 +542,7 @@ func (b *backend) checkConflict(res db.Result, key *resourcepb.ResourceKey, rv i } func (b *backend) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { - _, span := b.tracer.Start(ctx, tracePrefix+".Read") + _, span := tracer.Start(ctx, "sql.backend.ReadResource") defer span.End() // TODO: validate key ? @@ -580,7 +575,7 @@ func (b *backend) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) } func (b *backend) ListIterator(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"List") + ctx, span := tracer.Start(ctx, "sql.backend.ListIterator") defer span.End() if err := resource.MigrateListRequestVersionMatch(req, b.log); err != nil { @@ -602,7 +597,7 @@ func (b *backend) ListIterator(ctx context.Context, req *resourcepb.ListRequest, } func (b *backend) ListHistory(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"ListHistory") + ctx, span := tracer.Start(ctx, "sql.backend.ListHistory") defer span.End() return b.getHistory(ctx, req, cb) @@ -610,7 +605,7 @@ func (b *backend) ListHistory(ctx context.Context, req *resourcepb.ListRequest, // listLatest fetches the resources from the resource table. func (b *backend) listLatest(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"listLatest") + ctx, span := tracer.Start(ctx, "sql.backend.listLatest") defer span.End() if req.NextPageToken != "" { @@ -724,7 +719,7 @@ func (b *backend) ListModifiedSince(ctx context.Context, key resource.Namespaced // listAtRevision fetches the resources from the resource_history table at a specific revision. func (b *backend) listAtRevision(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"listAtRevision") + ctx, span := tracer.Start(ctx, "sql.backend.listAtRevision") defer span.End() // Get the RV @@ -784,7 +779,7 @@ func (b *backend) listAtRevision(ctx context.Context, req *resourcepb.ListReques // readHistory fetches the resource history from the resource_history table. func (b *backend) readHistory(ctx context.Context, key *resourcepb.ResourceKey, rv int64) *resource.BackendReadResponse { - _, span := b.tracer.Start(ctx, tracePrefix+".ReadHistory") + _, span := tracer.Start(ctx, "sql.backend.readHistory") defer span.End() readReq := &sqlResourceHistoryReadRequest{ @@ -815,7 +810,7 @@ func (b *backend) readHistory(ctx context.Context, key *resourcepb.ResourceKey, // getHistory fetches the resource history from the resource_history table. func (b *backend) getHistory(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"getHistory") + ctx, span := tracer.Start(ctx, "sql.backend.getHistory") defer span.End() listReq := sqlGetHistoryRequest{ SQLTemplate: sqltemplate.New(b.dialect), @@ -903,7 +898,7 @@ func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.Writte // listLatestRVs returns the latest resource version for each (Group, Resource) pair. func (b *backend) listLatestRVs(ctx context.Context) (groupResourceRV, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"listLatestRVs") + ctx, span := tracer.Start(ctx, "sql.backend.listLatestRVs") defer span.End() var grvs []*groupResourceVersion err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { @@ -932,14 +927,14 @@ func (b *backend) listLatestRVs(ctx context.Context) (groupResourceRV, error) { // fetchLatestRV returns the current maximum RV in the resource table func (b *backend) fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, group, resource string) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"fetchLatestRV") + ctx, span := tracer.Start(ctx, "sql.backend.fetchLatestRV") defer span.End() - res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ + res, err := dbutil.QueryRow(ctx, x, rvmanager.SqlResourceVersionGet, rvmanager.SqlResourceVersionGetRequest{ SQLTemplate: sqltemplate.New(d), Group: group, Resource: resource, ReadOnly: true, - Response: new(resourceVersionResponse), + Response: new(rvmanager.ResourceVersionResponse), }) if errors.Is(err, sql.ErrNoRows) { return 1, nil @@ -951,7 +946,7 @@ func (b *backend) fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqlte // fetchLatestHistoryRV returns the current maximum RV in the resource_history table func (b *backend) fetchLatestHistoryRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, key *resourcepb.ResourceKey, eventType resourcepb.WatchEvent_Type) (int64, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"fetchLatestHistoryRV") + ctx, span := tracer.Start(ctx, "sql.backend.fetchLatestHistoryRV") defer span.End() res, err := dbutil.QueryRow(ctx, x, sqlResourceHistoryReadLatestRV, sqlResourceHistoryReadLatestRVRequest{ SQLTemplate: sqltemplate.New(d), @@ -973,7 +968,7 @@ func (b *backend) fetchLatestHistoryRV(ctx context.Context, x db.ContextExecer, const limitLastImportTimesDeletion = 1 * time.Hour func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[resource.ResourceLastImportTime, error] { - ctx, span := b.tracer.Start(ctx, tracePrefix+"GetLastImportTimes") + ctx, span := tracer.Start(ctx, "sql.backend.GetResourceLastImportTimes") defer span.End() // Delete old entries, if configured, and if enough time has passed since last deletion. diff --git a/pkg/storage/unified/sql/backend_test.go b/pkg/storage/unified/sql/backend_test.go index f7ecd755f32..2f8f9fcb060 100644 --- a/pkg/storage/unified/sql/backend_test.go +++ b/pkg/storage/unified/sql/backend_test.go @@ -40,6 +40,26 @@ type testBackend struct { test.TestDBProvider } +func expectSuccessfulResourceVersionLock(t *testing.T, dbp test.TestDBProvider, rv int64, timestamp int64) { + dbp.SQLMock.ExpectQuery("select resource_version, unix_timestamp for update"). + WillReturnRows(sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}). + AddRow(rv, timestamp)) +} + +func expectSuccessfulResourceVersionSaveRV(t *testing.T, dbp test.TestDBProvider) { + dbp.SQLMock.ExpectExec("update resource set resource_version").WillReturnResult(sqlmock.NewResult(1, 1)) + dbp.SQLMock.ExpectExec("update resource_history set resource_version").WillReturnResult(sqlmock.NewResult(1, 1)) + dbp.SQLMock.ExpectExec("update resource_version set resource_version").WillReturnResult(sqlmock.NewResult(1, 1)) +} + +func expectSuccessfulResourceVersionExec(t *testing.T, dbp test.TestDBProvider, cbs ...func()) { + for _, cb := range cbs { + cb() + } + expectSuccessfulResourceVersionLock(t, dbp, 100, 200) + expectSuccessfulResourceVersionSaveRV(t, dbp) +} + func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) { b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected)) } diff --git a/pkg/storage/unified/sql/blob.go b/pkg/storage/unified/sql/blob.go index 3a1690fd580..64a14e7cbd8 100644 --- a/pkg/storage/unified/sql/blob.go +++ b/pkg/storage/unified/sql/blob.go @@ -27,7 +27,7 @@ func (b *backend) SupportsSignedURLs() bool { } func (b *backend) PutResourceBlob(ctx context.Context, req *resourcepb.PutBlobRequest) (*resourcepb.PutBlobResponse, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"PutResourceBlob") + ctx, span := tracer.Start(ctx, "sql.backend.PutResourceBlob") defer span.End() if req.Method == resourcepb.PutBlobRequest_HTTP { @@ -83,7 +83,7 @@ func (b *backend) PutResourceBlob(ctx context.Context, req *resourcepb.PutBlobRe } func (b *backend) GetResourceBlob(ctx context.Context, key *resourcepb.ResourceKey, info *utils.BlobInfo, mustProxy bool) (*resourcepb.GetBlobResponse, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"GetResourceBlob") + ctx, span := tracer.Start(ctx, "sql.backend.GetResourceBlob") defer span.End() if info == nil { diff --git a/pkg/storage/unified/sql/bulk.go b/pkg/storage/unified/sql/bulk.go index 6580975a764..4037c74f0dd 100644 --- a/pkg/storage/unified/sql/bulk.go +++ b/pkg/storage/unified/sql/bulk.go @@ -281,13 +281,13 @@ func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resou } if b.dialect.DialectName() == "sqlite" { - nextRV, err := b.rvManager.lock(ctx, tx, key.Group, key.Resource) + nextRV, err := b.rvManager.Lock(ctx, tx, key.Group, key.Resource) if err != nil { b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key)) } else { b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key)) // Save the incremented RV - if err := b.rvManager.saveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { + if err := b.rvManager.SaveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key)) } else { b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key)) diff --git a/pkg/storage/unified/sql/data/resource_history_update_rv.sql b/pkg/storage/unified/sql/data/resource_history_update_rv.sql deleted file mode 100644 index 767338ddb34..00000000000 --- a/pkg/storage/unified/sql/data/resource_history_update_rv.sql +++ /dev/null @@ -1,12 +0,0 @@ -UPDATE {{ .Ident "resource_history" }} -SET {{ .Ident "resource_version" }} = ( - CASE - {{ range $guid, $rv := .GUIDToRV }} - WHEN {{ $.Ident "guid" }} = {{ $.Arg $guid }} THEN CAST({{ $.Arg $rv }} AS {{ if eq $.DialectName "postgres" }}BIGINT{{ else }}SIGNED{{ end }}) - {{ end }} - END -) -WHERE {{ .Ident "guid" }} IN ( - {{$first := true}} - {{ range $guid, $rv := .GUIDToRV }}{{if $first}}{{$first = false}}{{else}}, {{end}}{{ $.Arg $guid }}{{ end }} -); diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index c4db9b1dbb9..0f19ae01d93 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -198,5 +198,24 @@ func initResourceTables(mg *migrator.Migrator) string { } mg.AddMigration("create table "+resource_events_table.Name, migrator.NewAddTableMigration(resource_events_table)) + mg.AddMigration("Add IDX_resource_history_key_path index", migrator.NewAddIndexMigration(resource_history_table, &migrator.Index{ + Cols: []string{"key_path"}, + Type: migrator.IndexType, + Name: "IDX_resource_history_key_path", + })) + + oldResourceVersionUniqueKey := migrator.Index{Cols: []string{"group", "resource"}, Type: migrator.UniqueIndex} + updatedResourceVersionTable := migrator.Table{ + Name: "resource_version", + Columns: []*migrator.Column{ + {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false, IsPrimaryKey: true}, + {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false, IsPrimaryKey: true}, + {Name: "resource_version", Type: migrator.DB_BigInt, Nullable: false}, + }, + PrimaryKeys: []string{"group", "resource"}, + } + + migrator.ConvertUniqueKeyToPrimaryKey(mg, oldResourceVersionUniqueKey, updatedResourceVersionTable) + return marker } diff --git a/pkg/storage/unified/sql/list_iterator_test.go b/pkg/storage/unified/sql/list_iterator_test.go index 3b567ca0f3e..e049e06525e 100644 --- a/pkg/storage/unified/sql/list_iterator_test.go +++ b/pkg/storage/unified/sql/list_iterator_test.go @@ -17,6 +17,7 @@ import ( dbsql "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" @@ -94,7 +95,7 @@ func TestIntegrationListIter(t *testing.T) { return fmt.Errorf("failed to insert test data: %w", err) } - if _, err = dbutil.Exec(ctx, tx, sqlResourceUpdateRV, sqlResourceUpdateRVRequest{ + if _, err = dbutil.Exec(ctx, tx, rvmanager.SqlResourceUpdateRV, rvmanager.SqlResourceUpdateRVRequest{ SQLTemplate: sqltemplate.New(dialect), GUIDToRV: map[string]int64{ item.guid: item.resourceVersion, diff --git a/pkg/storage/unified/sql/notifier.go b/pkg/storage/unified/sql/notifier.go index 803bf531cf3..5cd5d79ef77 100644 --- a/pkg/storage/unified/sql/notifier.go +++ b/pkg/storage/unified/sql/notifier.go @@ -28,7 +28,6 @@ func newNotifier(b *backend) (eventNotifier, error) { pollingInterval: b.pollingInterval, watchBufferSize: b.watchBufferSize, log: b.log, - tracer: b.tracer, bulkLock: b.bulkLock, listLatestRVs: b.listLatestRVs, storageMetrics: b.storageMetrics, diff --git a/pkg/storage/unified/sql/notifier_sql.go b/pkg/storage/unified/sql/notifier_sql.go index da06c5b6555..d5464c94d85 100644 --- a/pkg/storage/unified/sql/notifier_sql.go +++ b/pkg/storage/unified/sql/notifier_sql.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -19,7 +17,6 @@ var ( errHistoryPollRequired = fmt.Errorf("historyPoll is required") errListLatestRVsRequired = fmt.Errorf("listLatestRVs is required") errBulkLockRequired = fmt.Errorf("bulkLock is required") - errTracerRequired = fmt.Errorf("tracer is required") errLogRequired = fmt.Errorf("log is required") errInvalidWatchBufferSize = fmt.Errorf("watchBufferSize must be greater than 0") errInvalidPollingInterval = fmt.Errorf("pollingInterval must be greater than 0") @@ -34,7 +31,6 @@ type pollingNotifier struct { watchBufferSize int log logging.Logger - tracer trace.Tracer storageMetrics *resource.StorageMetrics bulkLock *bulkLock @@ -50,7 +46,6 @@ type pollingNotifierConfig struct { watchBufferSize int log logging.Logger - tracer trace.Tracer storageMetrics *resource.StorageMetrics bulkLock *bulkLock @@ -70,9 +65,6 @@ func (cfg *pollingNotifierConfig) validate() error { if cfg.bulkLock == nil { return errBulkLockRequired } - if cfg.tracer == nil { - return errTracerRequired - } if cfg.log == nil { return errLogRequired } @@ -100,7 +92,6 @@ func newPollingNotifier(cfg *pollingNotifierConfig) (*pollingNotifier, error) { pollingInterval: cfg.pollingInterval, watchBufferSize: cfg.watchBufferSize, log: cfg.log, - tracer: cfg.tracer, bulkLock: cfg.bulkLock, listLatestRVs: cfg.listLatestRVs, historyPoll: cfg.historyPoll, @@ -131,7 +122,7 @@ func (p *pollingNotifier) poller(ctx context.Context, since groupResourceRV, str case <-p.done: return case <-t.C: - ctx, span := p.tracer.Start(ctx, tracePrefix+"poller") + ctx, span := tracer.Start(ctx, "sql.pollingNotifier.poller") // List the latest RVs to see if any of those are not have been seen before. grv, err := p.listLatestRVs(ctx) if err != nil { @@ -174,7 +165,7 @@ func (p *pollingNotifier) poller(ctx context.Context, since groupResourceRV, str } func (p *pollingNotifier) poll(ctx context.Context, grp string, res string, since int64, stream chan<- *resource.WrittenEvent) (int64, error) { - ctx, span := p.tracer.Start(ctx, tracePrefix+"poll") + ctx, span := tracer.Start(ctx, "sql.pollingNotifier.poll") defer span.End() start := time.Now() diff --git a/pkg/storage/unified/sql/notifier_sql_test.go b/pkg/storage/unified/sql/notifier_sql_test.go index d0998d71d86..636db7050e2 100644 --- a/pkg/storage/unified/sql/notifier_sql_test.go +++ b/pkg/storage/unified/sql/notifier_sql_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" @@ -30,7 +29,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -44,7 +42,6 @@ func TestPollingNotifierConfig(t *testing.T) { config: &pollingNotifierConfig{ listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -60,7 +57,6 @@ func TestPollingNotifierConfig(t *testing.T) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -76,7 +72,6 @@ func TestPollingNotifierConfig(t *testing.T) { return nil, nil }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -85,22 +80,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, expectedErr: errBulkLockRequired, }, - { - name: "missing tracer", - config: &pollingNotifierConfig{ - historyPoll: func(ctx context.Context, grp string, res string, since int64) ([]*historyPollResponse, error) { - return nil, nil - }, - listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, - bulkLock: &bulkLock{}, - log: &logging.NoOpLogger{}, - watchBufferSize: 10, - pollingInterval: time.Second, - done: make(chan struct{}), - dialect: sqltemplate.SQLite, - }, - expectedErr: errTracerRequired, - }, { name: "missing logger", config: &pollingNotifierConfig{ @@ -109,7 +88,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), watchBufferSize: 10, pollingInterval: time.Second, done: make(chan struct{}), @@ -125,7 +103,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 0, pollingInterval: time.Second, @@ -142,7 +119,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: 0, @@ -159,7 +135,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -175,7 +150,6 @@ func TestPollingNotifierConfig(t *testing.T) { }, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, bulkLock: &bulkLock{}, - tracer: noop.NewTracerProvider().Tracer("test"), log: &logging.NoOpLogger{}, watchBufferSize: 10, pollingInterval: time.Second, @@ -255,7 +229,6 @@ func TestPollingNotifier(t *testing.T) { pollingInterval: 10 * time.Millisecond, watchBufferSize: 10, log: &logging.NoOpLogger{}, - tracer: noop.NewTracerProvider().Tracer("test"), bulkLock: &bulkLock{}, listLatestRVs: listLatestRVs, historyPoll: historyPoll, @@ -309,7 +282,6 @@ func TestPollingNotifier(t *testing.T) { pollingInterval: 10 * time.Millisecond, watchBufferSize: 10, log: &logging.NoOpLogger{}, - tracer: noop.NewTracerProvider().Tracer("test"), bulkLock: &bulkLock{}, listLatestRVs: listLatestRVs, historyPoll: historyPoll, @@ -343,7 +315,6 @@ func TestPollingNotifier(t *testing.T) { pollingInterval: 10 * time.Millisecond, watchBufferSize: 10, log: &logging.NoOpLogger{}, - tracer: noop.NewTracerProvider().Tracer("test"), bulkLock: &bulkLock{}, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, historyPoll: func(ctx context.Context, grp string, res string, since int64) ([]*historyPollResponse, error) { @@ -380,7 +351,6 @@ func TestPollingNotifier(t *testing.T) { pollingInterval: 10 * time.Millisecond, watchBufferSize: 10, log: &logging.NoOpLogger{}, - tracer: noop.NewTracerProvider().Tracer("test"), bulkLock: &bulkLock{}, listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, historyPoll: func(ctx context.Context, grp string, res string, since int64) ([]*historyPollResponse, error) { diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index e51cf943041..725c4617403 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -38,10 +38,8 @@ var ( sqlResourceList = mustTemplate("resource_list.sql") sqlResourceHistoryList = mustTemplate("resource_history_list.sql") sqlResourceHistoryListModifiedSince = mustTemplate("resource_history_list_since_modified.sql") - sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql") sqlResourceHistoryRead = mustTemplate("resource_history_read.sql") sqlResourceHistoryReadLatestRV = mustTemplate("resource_history_read_latest_rv.sql") - sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql") sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql") sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql") sqlResourceHistoryGet = mustTemplate("resource_history_get.sql") @@ -51,10 +49,7 @@ var ( sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql") // sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql") - sqlResourceVersionGet = mustTemplate("resource_version_get.sql") - sqlResourceVersionUpdate = mustTemplate("resource_version_update.sql") - sqlResourceVersionInsert = mustTemplate("resource_version_insert.sql") - sqlResourceVersionList = mustTemplate("resource_version_list.sql") + sqlResourceVersionList = mustTemplate("resource_version_list.sql") sqlResourceBlobInsert = mustTemplate("resource_blob_insert.sql") sqlResourceBlobQuery = mustTemplate("resource_blob_query.sql") @@ -365,59 +360,11 @@ func (r sqlResourceBlobQueryRequest) Validate() error { return nil } -// update RV - -type sqlResourceUpdateRVRequest struct { - sqltemplate.SQLTemplate - GUIDToRV map[string]int64 -} - -func (r sqlResourceUpdateRVRequest) Validate() error { - return nil // TODO -} - -// resource_version table requests. -type resourceVersionResponse struct { - ResourceVersion int64 - CurrentEpoch int64 -} - -func (r *resourceVersionResponse) Results() (*resourceVersionResponse, error) { - return r, nil -} - type groupResourceVersion struct { Group, Resource string ResourceVersion int64 } -type sqlResourceVersionUpsertRequest struct { - sqltemplate.SQLTemplate - Group, Resource string - ResourceVersion int64 -} - -func (r sqlResourceVersionUpsertRequest) Validate() error { - return nil // TODO -} - -type sqlResourceVersionGetRequest struct { - sqltemplate.SQLTemplate - Group, Resource string - ReadOnly bool - Response *resourceVersionResponse -} - -func (r sqlResourceVersionGetRequest) Validate() error { - return nil // TODO -} -func (r sqlResourceVersionGetRequest) Results() (*resourceVersionResponse, error) { - return &resourceVersionResponse{ - ResourceVersion: r.Response.ResourceVersion, - CurrentEpoch: r.Response.CurrentEpoch, - }, nil -} - type sqlResourceVersionListRequest struct { sqltemplate.SQLTemplate *groupResourceVersion diff --git a/pkg/storage/unified/sql/queries_test.go b/pkg/storage/unified/sql/queries_test.go index 5673bfad1e8..93e9b1f5af4 100644 --- a/pkg/storage/unified/sql/queries_test.go +++ b/pkg/storage/unified/sql/queries_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" ) @@ -162,10 +163,10 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, - sqlResourceUpdateRV: { + rvmanager.SqlResourceUpdateRV: { { Name: "single path", - Data: &sqlResourceUpdateRVRequest{ + Data: &rvmanager.SqlResourceUpdateRVRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), GUIDToRV: map[string]int64{ "guid1": 123, @@ -228,10 +229,10 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, - sqlResourceHistoryUpdateRV: { + rvmanager.SqlResourceHistoryUpdateRV: { { Name: "single path", - Data: &sqlResourceUpdateRVRequest{ + Data: &rvmanager.SqlResourceUpdateRVRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), GUIDToRV: map[string]int64{ "guid1": 123, @@ -334,23 +335,23 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, - sqlResourceVersionGet: { + rvmanager.SqlResourceVersionGet: { { Name: "single path", - Data: &sqlResourceVersionGetRequest{ + Data: &rvmanager.SqlResourceVersionGetRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), Resource: "resource", Group: "group", - Response: new(resourceVersionResponse), + Response: new(rvmanager.ResourceVersionResponse), ReadOnly: false, }, }, }, - sqlResourceVersionUpdate: { + rvmanager.SqlResourceVersionUpdate: { { Name: "increment resource version", - Data: &sqlResourceVersionUpsertRequest{ + Data: &rvmanager.SqlResourceVersionUpsertRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), Resource: "resource", Group: "group", @@ -359,10 +360,10 @@ func TestUnifiedStorageQueries(t *testing.T) { }, }, - sqlResourceVersionInsert: { + rvmanager.SqlResourceVersionInsert: { { Name: "single path", - Data: &sqlResourceVersionUpsertRequest{ + Data: &rvmanager.SqlResourceVersionUpsertRequest{ SQLTemplate: mocks.NewTestingSQLTemplate(), ResourceVersion: int64(12354), }, diff --git a/pkg/storage/unified/sql/rvmanager/data/resource_history_update_rv.sql b/pkg/storage/unified/sql/rvmanager/data/resource_history_update_rv.sql new file mode 100644 index 00000000000..266227b5317 --- /dev/null +++ b/pkg/storage/unified/sql/rvmanager/data/resource_history_update_rv.sql @@ -0,0 +1,31 @@ +UPDATE {{ .Ident "resource_history" }} +SET {{ .Ident "resource_version" }} = ( + CASE + {{ range $guid, $rv := .GUIDToRV }} + WHEN {{ $.Ident "guid" }} = {{ $.Arg $guid }} THEN CAST({{ $.Arg $rv }} AS {{ if eq $.DialectName "postgres" }}BIGINT{{ else }}SIGNED{{ end }}) + {{ end }} + END +), {{ .Ident "key_path" }} = ( + CASE + {{ range $guid, $snowflakeRv := .GUIDToSnowflakeRV }} + WHEN {{ $.Ident "guid" }} = {{ $.Arg $guid }} THEN CONCAT( + 'unified', {{ $.SlashFunc }}, 'data', {{ $.SlashFunc }}, + {{ $.Ident "group" }}, {{ $.SlashFunc }}, + {{ $.Ident "resource" }}, {{ $.SlashFunc }}, + {{ $.Ident "namespace" }}, {{ $.SlashFunc }}, + {{ $.Ident "name" }}, {{ $.SlashFunc }}, + CAST({{ $.Arg $snowflakeRv }} AS {{ if eq $.DialectName "postgres" }}BIGINT{{ else }}SIGNED{{ end }}), + {{ $.TildeFunc }}, + CASE {{ $.Ident "action" }} + WHEN 1 THEN 'created' + WHEN 2 THEN 'updated' + WHEN 3 THEN 'deleted' + END, {{ $.TildeFunc }}, + COALESCE({{ $.Ident "folder" }}, '')) + {{ end }} + END +) +WHERE {{ .Ident "guid" }} IN ( + {{$first := true}} + {{ range $guid, $rv := .GUIDToRV }}{{if $first}}{{$first = false}}{{else}}, {{end}}{{ $.Arg $guid }}{{ end }} +); diff --git a/pkg/storage/unified/sql/data/resource_update_rv.sql b/pkg/storage/unified/sql/rvmanager/data/resource_update_rv.sql similarity index 100% rename from pkg/storage/unified/sql/data/resource_update_rv.sql rename to pkg/storage/unified/sql/rvmanager/data/resource_update_rv.sql diff --git a/pkg/storage/unified/sql/data/resource_version_get.sql b/pkg/storage/unified/sql/rvmanager/data/resource_version_get.sql similarity index 100% rename from pkg/storage/unified/sql/data/resource_version_get.sql rename to pkg/storage/unified/sql/rvmanager/data/resource_version_get.sql diff --git a/pkg/storage/unified/sql/data/resource_version_insert.sql b/pkg/storage/unified/sql/rvmanager/data/resource_version_insert.sql similarity index 100% rename from pkg/storage/unified/sql/data/resource_version_insert.sql rename to pkg/storage/unified/sql/rvmanager/data/resource_version_insert.sql diff --git a/pkg/storage/unified/sql/data/resource_version_update.sql b/pkg/storage/unified/sql/rvmanager/data/resource_version_update.sql similarity index 100% rename from pkg/storage/unified/sql/data/resource_version_update.sql rename to pkg/storage/unified/sql/rvmanager/data/resource_version_update.sql diff --git a/pkg/storage/unified/sql/rvmanager/queries.go b/pkg/storage/unified/sql/rvmanager/queries.go new file mode 100644 index 00000000000..c0411cdf704 --- /dev/null +++ b/pkg/storage/unified/sql/rvmanager/queries.go @@ -0,0 +1,84 @@ +package rvmanager + +import ( + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +type SqlResourceUpdateRVRequest struct { + sqltemplate.SQLTemplate + GUIDToRV map[string]int64 + GUIDToSnowflakeRV map[string]int64 +} + +func (r SqlResourceUpdateRVRequest) Validate() error { + return nil // TODO +} + +func (r SqlResourceUpdateRVRequest) SlashFunc() string { + if r.DialectName() == "postgres" { + return "CHR(47)" + } + + return "CHAR(47)" +} + +func (r SqlResourceUpdateRVRequest) TildeFunc() string { + if r.DialectName() == "postgres" { + return "CHR(126)" + } + + return "CHAR(126)" +} + +type ResourceVersionResponse struct { + ResourceVersion int64 + CurrentEpoch int64 +} + +func (r *ResourceVersionResponse) Results() (*ResourceVersionResponse, error) { + return r, nil +} + +type sqlResourceVersionGetRequest struct { + sqltemplate.SQLTemplate + Group, Resource string + ReadOnly bool + Response *ResourceVersionResponse +} + +func (r sqlResourceVersionGetRequest) Validate() error { + return nil // TODO +} +func (r sqlResourceVersionGetRequest) Results() (*ResourceVersionResponse, error) { + return &ResourceVersionResponse{ + ResourceVersion: r.Response.ResourceVersion, + CurrentEpoch: r.Response.CurrentEpoch, + }, nil +} + +type SqlResourceVersionUpsertRequest struct { + sqltemplate.SQLTemplate + Group, Resource string + ResourceVersion int64 +} + +func (r SqlResourceVersionUpsertRequest) Validate() error { + return nil // TODO +} + +type SqlResourceVersionGetRequest struct { + sqltemplate.SQLTemplate + Group, Resource string + ReadOnly bool + Response *ResourceVersionResponse +} + +func (r SqlResourceVersionGetRequest) Validate() error { + return nil // TODO +} +func (r SqlResourceVersionGetRequest) Results() (*ResourceVersionResponse, error) { + return &ResourceVersionResponse{ + ResourceVersion: r.Response.ResourceVersion, + CurrentEpoch: r.Response.CurrentEpoch, + }, nil +} diff --git a/pkg/storage/unified/sql/rv_manager.go b/pkg/storage/unified/sql/rvmanager/rv_manager.go similarity index 80% rename from pkg/storage/unified/sql/rv_manager.go rename to pkg/storage/unified/sql/rvmanager/rv_manager.go index 4c6a092d937..b10685f22ad 100644 --- a/pkg/storage/unified/sql/rv_manager.go +++ b/pkg/storage/unified/sql/rvmanager/rv_manager.go @@ -1,4 +1,4 @@ -package sql +package rvmanager import ( "context" @@ -8,11 +8,12 @@ import ( "sync" "time" + "github.com/bwmarrin/snowflake" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" @@ -20,6 +21,21 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) +type contextKey string + +const txKey contextKey = "rvmanager_db_tx" + +func ContextWithTx(ctx context.Context, tx db.Tx) context.Context { + return context.WithValue(ctx, txKey, tx) +} + +func TxFromCtx(ctx context.Context) (db.Tx, bool) { + tx, ok := ctx.Value(txKey).(db.Tx) + return tx, ok +} + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager") + var ( rvmWriteDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "rvmanager_write_duration_seconds", @@ -62,11 +78,10 @@ const ( defaultBatchTimeout = 5 * time.Second ) -// resourceVersionManager handles resource version operations -type resourceVersionManager struct { +// ResourceVersionManager handles resource version operations +type ResourceVersionManager struct { dialect sqltemplate.Dialect db db.DB - tracer trace.Tracer batchMu sync.RWMutex batchChMap map[string]chan *writeOp @@ -98,30 +113,25 @@ type ResourceManagerOptions struct { DB db.DB // The database to use MaxBatchSize int // The maximum number of operations to batch together MaxBatchWaitTime time.Duration // The maximum time to wait for a batch to be ready - Tracer trace.Tracer // The tracer to use for tracing } // NewResourceVersionManager creates a new ResourceVersionManager -func NewResourceVersionManager(opts ResourceManagerOptions) (*resourceVersionManager, error) { +func NewResourceVersionManager(opts ResourceManagerOptions) (*ResourceVersionManager, error) { if opts.MaxBatchSize == 0 { opts.MaxBatchSize = defaultMaxBatchSize } if opts.MaxBatchWaitTime == 0 { opts.MaxBatchWaitTime = defaultMaxBatchWaitTime } - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("resource-version-manager") - } if opts.Dialect == nil { return nil, errors.New("dialect is required") } if opts.DB == nil { return nil, errors.New("db is required") } - return &resourceVersionManager{ + return &ResourceVersionManager{ dialect: opts.Dialect, db: opts.DB, - tracer: opts.Tracer, batchChMap: make(map[string]chan *writeOp), maxBatchSize: opts.MaxBatchSize, maxBatchWaitTime: opts.MaxBatchWaitTime, @@ -129,7 +139,7 @@ func NewResourceVersionManager(opts ResourceManagerOptions) (*resourceVersionMan } // ExecWithRV executes the given function with an incremented resource version -func (m *resourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb.ResourceKey, fn WriteEventFunc) (rv int64, err error) { +func (m *ResourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb.ResourceKey, fn WriteEventFunc) (rv int64, err error) { rvmInflightWrites.WithLabelValues(key.Group, key.Resource).Inc() defer rvmInflightWrites.WithLabelValues(key.Group, key.Resource).Dec() @@ -143,7 +153,7 @@ func (m *resourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb })) defer timer.ObserveDuration() - ctx, span := m.tracer.Start(ctx, "sql.rvmanager.ExecWithRV") + ctx, span := tracer.Start(ctx, "sql.resourceVersionManager.ExecWithRV") defer span.End() span.SetAttributes( @@ -185,7 +195,7 @@ func (m *resourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb } // startBatchProcessor is responsible for processing batches of write operations -func (m *resourceVersionManager) startBatchProcessor(group, resource string) { +func (m *ResourceVersionManager) startBatchProcessor(group, resource string) { ctx := context.TODO() batchKey := fmt.Sprintf("%s/%s", group, resource) @@ -222,8 +232,12 @@ func (m *resourceVersionManager) startBatchProcessor(group, resource string) { } } -func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource string, batch []writeOp) { - ctx, span := m.tracer.Start(ctx, "sql.rvmanager.execBatch") +var readCommitted = &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, +} + +func (m *ResourceVersionManager) execBatch(ctx context.Context, group, resource string, batch []writeOp) { + ctx, span := tracer.Start(ctx, "sql.resourceVersionManager.execBatch") defer span.End() // Add batch size attribute @@ -247,10 +261,11 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource defer cancel() guidToRV := make(map[string]int64, len(batch)) + guidToSnowflakeRV := make(map[string]int64, len(batch)) guids := make([]string, len(batch)) // The GUIDs of the created resources in the same order as the batch rvs := make([]int64, len(batch)) // The RVs of the created resources in the same order as the batch - err = m.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { + err = m.db.WithTx(ctx, readCommitted, func(ctx context.Context, tx db.Tx) error { span.AddEvent("starting_batch_transaction") writeTimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { @@ -273,7 +288,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource lockTimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { rvmExecBatchPhaseDuration.WithLabelValues(group, resource, "waiting_for_lock").Observe(v) })) - rv, err := m.lock(ctx, tx, group, resource) + rv, err := m.Lock(ctx, tx, group, resource) lockTimer.ObserveDuration() if err != nil { span.AddEvent("resource_version_lock_failed", trace.WithAttributes( @@ -292,11 +307,12 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource // Allocate the RVs for i, guid := range guids { guidToRV[guid] = rv + guidToSnowflakeRV[guid] = SnowflakeFromRv(rv) rvs[i] = rv rv++ } // Update the resource version for the created resources in both the resource and the resource history - if _, err := dbutil.Exec(ctx, tx, sqlResourceUpdateRV, sqlResourceUpdateRVRequest{ + if _, err := dbutil.Exec(ctx, tx, SqlResourceUpdateRV, SqlResourceUpdateRVRequest{ SQLTemplate: sqltemplate.New(m.dialect), GUIDToRV: guidToRV, }); err != nil { @@ -307,9 +323,10 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource } span.AddEvent("resource_versions_updated") - if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryUpdateRV, sqlResourceUpdateRVRequest{ - SQLTemplate: sqltemplate.New(m.dialect), - GUIDToRV: guidToRV, + if _, err := dbutil.Exec(ctx, tx, SqlResourceHistoryUpdateRV, SqlResourceUpdateRVRequest{ + SQLTemplate: sqltemplate.New(m.dialect), + GUIDToRV: guidToRV, + GUIDToSnowflakeRV: guidToSnowflakeRV, }); err != nil { span.AddEvent("resource_history_update_rv_failed", trace.WithAttributes( attribute.String("error", err.Error()), @@ -319,7 +336,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource span.AddEvent("resource_history_versions_updated") // Record the latest RV in the resource version table - err = m.saveRV(ctx, tx, group, resource, rv) + err = m.SaveRV(ctx, tx, group, resource, rv) if err != nil { span.AddEvent("save_rv_failed", trace.WithAttributes( attribute.String("error", err.Error()), @@ -347,20 +364,36 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource } } -// lock locks the resource version for the given key -func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, group, resource string) (nextRV int64, err error) { +// takes a unix microsecond rv and transforms into a snowflake format. The timestamp is converted from microsecond to +// millisecond (the integer division) and the remainder is saved in the stepbits section. machine id is always 0 +func SnowflakeFromRv(rv int64) int64 { + return (((rv / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (rv % 1000) +} + +// helper utility to compare two RVs. The first RV must be in snowflake format. Will convert rv2 to snowflake and retry +// if comparison fails +func IsRvEqual(rv1, rv2 int64) bool { + if rv1 == rv2 { + return true + } + + return rv1 == SnowflakeFromRv(rv2) +} + +// Lock locks the resource version for the given key +func (m *ResourceVersionManager) Lock(ctx context.Context, x db.ContextExecer, group, resource string) (nextRV int64, err error) { // 1. Lock the row and prevent concurrent updates until the transaction is committed - res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ + res, err := dbutil.QueryRow(ctx, x, SqlResourceVersionGet, sqlResourceVersionGetRequest{ SQLTemplate: sqltemplate.New(m.dialect), Group: group, Resource: resource, - Response: new(resourceVersionResponse), + Response: new(ResourceVersionResponse), ReadOnly: false, // Lock the row for update }) if errors.Is(err, sql.ErrNoRows) { // If there wasn't a row for this resource, create it - if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionUpsertRequest{ + if _, err = dbutil.Exec(ctx, x, SqlResourceVersionInsert, SqlResourceVersionUpsertRequest{ SQLTemplate: sqltemplate.New(m.dialect), Group: group, Resource: resource, @@ -369,11 +402,11 @@ func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, g } // Fetch the newly created resource version - res, err = dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{ + res, err = dbutil.QueryRow(ctx, x, SqlResourceVersionGet, sqlResourceVersionGetRequest{ SQLTemplate: sqltemplate.New(m.dialect), Group: group, Resource: resource, - Response: new(resourceVersionResponse), + Response: new(ResourceVersionResponse), ReadOnly: true, }) if err != nil { @@ -387,8 +420,8 @@ func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, g return max(res.CurrentEpoch, res.ResourceVersion+1), nil } -func (m *resourceVersionManager) saveRV(ctx context.Context, x db.ContextExecer, group, resource string, rv int64) error { - _, err := dbutil.Exec(ctx, x, sqlResourceVersionUpdate, sqlResourceVersionUpsertRequest{ +func (m *ResourceVersionManager) SaveRV(ctx context.Context, x db.ContextExecer, group, resource string, rv int64) error { + _, err := dbutil.Exec(ctx, x, SqlResourceVersionUpdate, SqlResourceVersionUpsertRequest{ SQLTemplate: sqltemplate.New(m.dialect), Group: group, Resource: resource, diff --git a/pkg/storage/unified/sql/rv_manager_test.go b/pkg/storage/unified/sql/rvmanager/rv_manager_test.go similarity index 99% rename from pkg/storage/unified/sql/rv_manager_test.go rename to pkg/storage/unified/sql/rvmanager/rv_manager_test.go index 46d23d26a59..9a2e105aa2f 100644 --- a/pkg/storage/unified/sql/rv_manager_test.go +++ b/pkg/storage/unified/sql/rvmanager/rv_manager_test.go @@ -1,4 +1,4 @@ -package sql +package rvmanager import ( "testing" diff --git a/pkg/storage/unified/sql/rvmanager/templates.go b/pkg/storage/unified/sql/rvmanager/templates.go new file mode 100644 index 00000000000..65c91a273af --- /dev/null +++ b/pkg/storage/unified/sql/rvmanager/templates.go @@ -0,0 +1,30 @@ +package rvmanager + +import ( + "embed" + "fmt" + "text/template" +) + +// Templates setup. +var ( + //go:embed data/*.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`)) +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +var ( + SqlResourceUpdateRV = mustTemplate("resource_update_rv.sql") + SqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql") + SqlResourceVersionGet = mustTemplate("resource_version_get.sql") + SqlResourceVersionUpdate = mustTemplate("resource_version_update.sql") + SqlResourceVersionInsert = mustTemplate("resource_version_insert.sql") +) diff --git a/pkg/storage/unified/sql/search.go b/pkg/storage/unified/sql/search.go index e10c3883f54..d2d0624cf53 100644 --- a/pkg/storage/unified/sql/search.go +++ b/pkg/storage/unified/sql/search.go @@ -18,7 +18,7 @@ var _ resourcepb.ResourceIndexServer = &backend{} // GetStats implements resource.ResourceIndexServer. // This will use the SQL index to count values func (b *backend) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"GetStats") + ctx, span := tracer.Start(ctx, "sql.backend.GetStats") defer span.End() sreq := &sqlStatsRequest{ diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index f91baa7a695..f4a1ee3ce77 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -20,6 +20,8 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) type QOSEnqueueDequeuer interface { @@ -70,7 +72,7 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) { SecureValues: opts.SecureValues, } if opts.AccessClient != nil { - serverOptions.AccessClient = resource.NewAuthzLimitedClient(opts.AccessClient, resource.AuthzOptions{Tracer: opts.Tracer, Registry: opts.Reg}) + serverOptions.AccessClient = resource.NewAuthzLimitedClient(opts.AccessClient, resource.AuthzOptions{Registry: opts.Reg}) } // Support local file blob if strings.HasPrefix(serverOptions.Blob.URL, "./data/") { @@ -97,23 +99,64 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) { return nil, err } - isHA := isHighAvailabilityEnabled(opts.Cfg.SectionWithEnvOverrides("database"), - opts.Cfg.SectionWithEnvOverrides("resource_api")) + if opts.Cfg.EnableSQLKVBackend { + sqlkv, err := resource.NewSQLKV(eDB) + if err != nil { + return nil, fmt.Errorf("error creating sqlkv: %s", err) + } - backend, err := NewBackend(BackendOptions{ - DBProvider: eDB, - Tracer: opts.Tracer, - Reg: opts.Reg, - IsHA: isHA, - storageMetrics: opts.StorageMetrics, - LastImportTimeMaxAge: opts.SearchOptions.MaxIndexAge, // No need to keep last_import_times older than max index age. - }) - if err != nil { - return nil, err + kvBackendOpts := resource.KVBackendOptions{ + KvStore: sqlkv, + Tracer: opts.Tracer, + Reg: opts.Reg, + } + + ctx := context.Background() + dbConn, err := eDB.Init(ctx) + if err != nil { + return nil, fmt.Errorf("error initializing DB: %w", err) + } + + dialect := sqltemplate.DialectForDriver(dbConn.DriverName()) + if dialect == nil { + return nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName()) + } + + rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{ + Dialect: dialect, + DB: dbConn, + }) + if err != nil { + return nil, fmt.Errorf("failed to create resource version manager: %w", err) + } + + // TODO add config to decide whether to pass RvManager or not + kvBackendOpts.RvManager = rvManager + kvBackend, err := resource.NewKVStorageBackend(kvBackendOpts) + if err != nil { + return nil, fmt.Errorf("error creating kv backend: %s", err) + } + + serverOptions.Backend = kvBackend + serverOptions.Diagnostics = kvBackend + } else { + isHA := isHighAvailabilityEnabled(opts.Cfg.SectionWithEnvOverrides("database"), + opts.Cfg.SectionWithEnvOverrides("resource_api")) + + backend, err := NewBackend(BackendOptions{ + DBProvider: eDB, + Reg: opts.Reg, + IsHA: isHA, + storageMetrics: opts.StorageMetrics, + LastImportTimeMaxAge: opts.SearchOptions.MaxIndexAge, // No need to keep last_import_times older than max index age. + }) + if err != nil { + return nil, err + } + serverOptions.Backend = backend + serverOptions.Diagnostics = backend + serverOptions.Lifecycle = backend } - serverOptions.Backend = backend - serverOptions.Diagnostics = backend - serverOptions.Lifecycle = backend } serverOptions.Search = opts.SearchOptions diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 334c2dfee76..75b3e80fcb0 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -260,7 +260,7 @@ func (s *service) starting(ctx context.Context) error { return err } - searchOptions, err := search.NewSearchOptions(s.features, s.cfg, s.tracing, s.docBuilders, s.indexMetrics, s.OwnsIndex) + searchOptions, err := search.NewSearchOptions(s.features, s.cfg, s.docBuilders, s.indexMetrics, s.OwnsIndex) if err != nil { return err } @@ -312,6 +312,7 @@ func (s *service) starting(ctx context.Context) error { resourcepb.RegisterManagedObjectIndexServer(srv, server) resourcepb.RegisterBlobStoreServer(srv, server) resourcepb.RegisterDiagnosticsServer(srv, server) + resourcepb.RegisterQuotasServer(srv, server) grpc_health_v1.RegisterHealthServer(srv, healthService) // register reflection service diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go index f9665c83b7d..8bf65cbd6f7 100644 --- a/pkg/storage/unified/sql/test/benchmark_test.go +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -15,5 +15,6 @@ func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { if db.IsTestDbSQLite() { opts.Concurrency = 1 // to avoid SQLite database is locked error } - test.BenchmarkStorageBackend(t, newTestBackend(t, true, 2*time.Millisecond), opts) + backend, _ := newTestBackend(t, true, 2*time.Millisecond) + test.BenchmarkStorageBackend(t, backend, opts) } diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index d32ca569d66..eaf78de0779 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" + sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" unitest "github.com/grafana/grafana/pkg/storage/unified/testing" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -38,7 +39,7 @@ var initMutex = &sync.Mutex{} // newTestBackend creates a fresh database and backend for a test. // It uses a mutex to ensure the entire initialization and migration // process is atomic and does not race with other parallel tests. -func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Duration) resource.StorageBackend { +func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Duration) (resource.StorageBackend, sqldb.DB) { // Lock to ensure the entire init block is atomic. initMutex.Lock() // Unlock once the function returns the initialized backend. @@ -61,7 +62,11 @@ func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Durati // Use a context with a reasonable timeout for migrations. err = backend.Init(testutil.NewTestContext(t, time.Now().Add(1*time.Minute))) require.NoError(t, err) - return backend + + sqlDB, err := eDB.Init(testutil.NewTestContext(t, time.Now().Add(1*time.Minute))) + require.NoError(t, err) + + return backend, sqlDB } func TestMain(m *testing.M) { @@ -73,7 +78,8 @@ func TestIntegrationStorageServer(t *testing.T) { t.Cleanup(db.CleanupTestDB) unitest.RunStorageServerTest(t, func(ctx context.Context) resource.StorageBackend { - return newTestBackend(t, true, 0) + backend, _ := newTestBackend(t, true, 0) + return backend }) } @@ -84,12 +90,31 @@ func TestIntegrationSQLStorageBackend(t *testing.T) { t.Run("IsHA (polling notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { - return newTestBackend(t, true, 0) + backend, _ := newTestBackend(t, true, 0) + return backend }, nil) }) t.Run("NotHA (in process notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + backend, _ := newTestBackend(t, false, 0) + return backend + }, nil) + }) +} + +func TestIntegrationSQLStorageAndSQLKVCompatibilityTests(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + t.Cleanup(db.CleanupTestDB) + + t.Run("IsHA (polling notifier)", func(t *testing.T) { + unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) { + return newTestBackend(t, true, 0) + }, nil) + }) + + t.Run("NotHA (in process notifier)", func(t *testing.T) { + unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) { return newTestBackend(t, false, 0) }, nil) }) @@ -104,13 +129,13 @@ func TestIntegrationSearchAndStorage(t *testing.T) { search, err := search.NewBleveBackend(search.BleveOptions{ FileThreshold: 0, Root: t.TempDir(), - }, tracing.NewNoopTracerService(), nil) + }, nil) require.NoError(t, err) require.NotNil(t, search) t.Cleanup(search.Stop) // Create a new resource backend - storage := newTestBackend(t, false, 0) + storage, _ := newTestBackend(t, false, 0) require.NotNil(t, storage) // Run the shared storage and search tests diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql index 6b07ff5e55b..62bb64c8300 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET `resource_version` = ( WHEN `guid` = 'guid1' THEN CAST(123 AS SIGNED) WHEN `guid` = 'guid2' THEN CAST(456 AS SIGNED) END +), `key_path` = ( + CASE + END ) WHERE `guid` IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql index 85352e9ead4..6529fb1e4d5 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET "resource_version" = ( WHEN "guid" = 'guid1' THEN CAST(123 AS BIGINT) WHEN "guid" = 'guid2' THEN CAST(456 AS BIGINT) END +), "key_path" = ( + CASE + END ) WHERE "guid" IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql index 9fb23fb3956..707fbad6cb8 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET "resource_version" = ( WHEN "guid" = 'guid1' THEN CAST(123 AS SIGNED) WHEN "guid" = 'guid2' THEN CAST(456 AS SIGNED) END +), "key_path" = ( + CASE + END ) WHERE "guid" IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/testing/kv.go b/pkg/storage/unified/testing/kv.go index eab9aa9c845..2031a3e38b2 100644 --- a/pkg/storage/unified/testing/kv.go +++ b/pkg/storage/unified/testing/kv.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "io" + "maps" + "slices" "strings" "testing" "time" @@ -28,6 +30,10 @@ const ( TestKVUnixTimestamp = "unix timestamp" TestKVBatchGet = "batch get operations" TestKVBatchDelete = "batch delete operations" + + // Use `eventsSection` as the section for the tests, as the sqlkv implementation + // needs a real section to determine which table to use. + testSection = "unified/events" ) // NewKVFunc is a function that creates a new KV instance for testing @@ -35,7 +41,8 @@ type NewKVFunc func(ctx context.Context) resource.KV // KVTestOptions configures which tests to run type KVTestOptions struct { - NSPrefix string // namespace prefix for isolation + SkipTests map[string]bool + NSPrefix string // namespace prefix for isolation } // GenerateRandomKVPrefix creates a random namespace prefix for test isolation @@ -72,23 +79,42 @@ func RunKVTest(t *testing.T, newKV NewKVFunc, opts *KVTestOptions) { } for _, tc := range cases { + if shouldSkip := opts.SkipTests[tc.name]; shouldSkip { + t.Logf("Skipping test: %s", tc.name) + continue + } + t.Run(tc.name, func(t *testing.T) { tc.fn(t, newKV(context.Background()), opts.NSPrefix) }) } } +func namespacedKeys(nsPrefix string, keys []string) []string { + prefixed := make([]string, 0, len(keys)) + for _, k := range keys { + prefixed = append(prefixed, nsPrefix+"/"+k) + } + + return prefixed +} + +func namespacedKey(nsPrefix, key string) string { + return namespacedKeys(nsPrefix, []string{key})[0] +} + func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-get" + nsPrefix += "-get" t.Run("get existing key", func(t *testing.T) { // First save a key + existingKey := namespacedKey(nsPrefix, "existing-key") testValue := "test value for get" - saveKVHelper(t, kv, ctx, section, "existing-key", strings.NewReader(testValue)) + saveKVHelper(t, kv, ctx, testSection, existingKey, strings.NewReader(testValue)) // Now get it - reader, err := kv.Get(ctx, section, "existing-key") + reader, err := kv.Get(ctx, testSection, existingKey) require.NoError(t, err) // Read the value @@ -102,28 +128,35 @@ func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) { }) t.Run("get non-existent key", func(t *testing.T) { - _, err := kv.Get(ctx, section, "non-existent-key") + _, err := kv.Get(ctx, testSection, namespacedKey(nsPrefix, "non-existent-key")) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) }) t.Run("get with empty section", func(t *testing.T) { - _, err := kv.Get(ctx, "", "some-key") + _, err := kv.Get(ctx, "", namespacedKey(nsPrefix, "some-key")) assert.Error(t, err) assert.Contains(t, err.Error(), "section is required") }) + + t.Run("get with empty key", func(t *testing.T) { + _, err := kv.Get(ctx, testSection, "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "key is required") + }) } func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-save" + nsPrefix += "-save" t.Run("save new key", func(t *testing.T) { + newKey := namespacedKey(nsPrefix, "new-key") testValue := "new test value" - saveKVHelper(t, kv, ctx, section, "new-key", strings.NewReader(testValue)) + saveKVHelper(t, kv, ctx, testSection, newKey, strings.NewReader(testValue)) // Verify it was saved - reader, err := kv.Get(ctx, section, "new-key") + reader, err := kv.Get(ctx, testSection, newKey) require.NoError(t, err) value, err := io.ReadAll(reader) @@ -134,15 +167,39 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { }) t.Run("save overwrite existing key", func(t *testing.T) { + overwriteKey := namespacedKey(nsPrefix, "overwrite-key") + // First save - saveKVHelper(t, kv, ctx, section, "overwrite-key", strings.NewReader("old value")) + saveKVHelper(t, kv, ctx, testSection, overwriteKey, strings.NewReader("old value")) // Overwrite newValue := "new value" - saveKVHelper(t, kv, ctx, section, "overwrite-key", strings.NewReader(newValue)) + saveKVHelper(t, kv, ctx, testSection, overwriteKey, strings.NewReader(newValue)) // Verify it was updated - reader, err := kv.Get(ctx, section, "overwrite-key") + reader, err := kv.Get(ctx, testSection, overwriteKey) + require.NoError(t, err) + + value, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, newValue, string(value)) + err = reader.Close() + require.NoError(t, err) + }) + + t.Run("save overwrite existing key (datastore)", func(t *testing.T) { + section := "unified/data" + overwriteKey := namespacedKey(nsPrefix, "overwrite-key") + + // First save + saveKVHelper(t, kv, ctx, section, overwriteKey, strings.NewReader("old value")) + + // Overwrite + newValue := "new value" + saveKVHelper(t, kv, ctx, section, overwriteKey, strings.NewReader(newValue)) + + // Verify it was updated + reader, err := kv.Get(ctx, section, overwriteKey) require.NoError(t, err) value, err := io.ReadAll(reader) @@ -159,11 +216,13 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { }) t.Run("save binary data", func(t *testing.T) { + binaryKey := namespacedKey(nsPrefix, "binary-key") + binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD} - saveKVHelper(t, kv, ctx, section, "binary-key", bytes.NewReader(binaryData)) + saveKVHelper(t, kv, ctx, testSection, binaryKey, bytes.NewReader(binaryData)) // Verify binary data - reader, err := kv.Get(ctx, section, "binary-key") + reader, err := kv.Get(ctx, testSection, binaryKey) require.NoError(t, err) value, err := io.ReadAll(reader) @@ -174,11 +233,13 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { }) t.Run("save key with no data", func(t *testing.T) { + emptyKey := namespacedKey(nsPrefix, "empty-key") + // Save a key with empty data - saveKVHelper(t, kv, ctx, section, "empty-key", strings.NewReader("")) + saveKVHelper(t, kv, ctx, testSection, emptyKey, strings.NewReader("")) // Verify it was saved with empty data - reader, err := kv.Get(ctx, section, "empty-key") + reader, err := kv.Get(ctx, testSection, emptyKey) require.NoError(t, err) value, err := io.ReadAll(reader) @@ -192,52 +253,59 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-delete" + nsPrefix += "-delete" t.Run("delete existing key", func(t *testing.T) { // First create a key - saveKVHelper(t, kv, ctx, section, "delete-key", strings.NewReader("delete me")) + deleteKey := namespacedKey(nsPrefix, "delete-key") + saveKVHelper(t, kv, ctx, testSection, deleteKey, strings.NewReader("delete me")) // Verify it exists - _, err := kv.Get(ctx, section, "delete-key") + _, err := kv.Get(ctx, testSection, deleteKey) require.NoError(t, err) // Delete it - err = kv.Delete(ctx, section, "delete-key") + err = kv.Delete(ctx, testSection, deleteKey) require.NoError(t, err) // Verify it's gone - _, err = kv.Get(ctx, section, "delete-key") + _, err = kv.Get(ctx, testSection, deleteKey) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) }) t.Run("delete non-existent key", func(t *testing.T) { - err := kv.Delete(ctx, section, "non-existent-delete-key") + err := kv.Delete(ctx, testSection, namespacedKey(nsPrefix, "non-existent-delete-key")) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) }) t.Run("delete with empty section", func(t *testing.T) { - err := kv.Delete(ctx, "", "some-key") + err := kv.Delete(ctx, "", namespacedKey(nsPrefix, "some-key")) assert.Error(t, err) assert.Contains(t, err.Error(), "section is required") }) + + t.Run("delete with empty key", func(t *testing.T) { + err := kv.Delete(ctx, testSection, "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "key is required") + }) } func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-keys" + nsPrefix += "-keys" // Setup test data - testKeys := []string{"a1", "a2", "b1", "b2", "c1"} + testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"}) for _, key := range testKeys { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) + saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key)) } t.Run("list all keys", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{}) { + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) { require.NoError(t, err) keys = append(keys, k) } @@ -250,261 +318,321 @@ func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) { for k, err := range kv.Keys(ctx, "", resource.ListOptions{}) { if err != nil { errors = append(errors, err) - break + continue } keys = append(keys, k) } - assert.Len(t, errors, 1) + require.Len(t, errors, 1) assert.Contains(t, errors[0].Error(), "section is required") assert.Empty(t, keys) }) + t.Run("invalid sort option, defaults to asc", func(t *testing.T) { + var keys []string + var errors []error + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + Sort: resource.SortOrder(100), + }) { + if err != nil { + errors = append(errors, err) + continue + } + keys = append(keys, k) + } + assert.Empty(t, errors) + assert.Equal(t, testKeys, keys) + }) + + t.Run("list keys with end key < start key", func(t *testing.T) { + var keys []string + var errors []error + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: namespacedKey(nsPrefix, "c"), + EndKey: namespacedKey(nsPrefix, "a"), + }) { + if err != nil { + errors = append(errors, err) + continue + } + keys = append(keys, k) + } + // Nothing is yielded + assert.Empty(t, errors) + assert.Empty(t, keys) + }) + t.Run("list keys returns 0 keys", func(t *testing.T) { - // Use a different section with no keys - emptySection := nsPrefix + "-empty-keys" + // Use a key range with no keys. + startKey, endKey := "aaaaa", "aaaaz" var keys []string - for k, err := range kv.Keys(ctx, emptySection, resource.ListOptions{}) { + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: startKey, + EndKey: endKey, + }) { require.NoError(t, err) keys = append(keys, k) } assert.Empty(t, keys) assert.Len(t, keys, 0) }) + + t.Run("interrupting the iterator", func(t *testing.T) { + var keys []string + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) { + require.NoError(t, err) + keys = append(keys, k) + + if len(keys) == 2 { + break + } + } + + assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2"}), keys) + }) } func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-keys-limits" + nsPrefix += "-keys-with-limits" // Setup test data - testKeys := []string{"a1", "a2", "b1", "b2", "c1", "c2", "d1", "d2"} + testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1", "c2", "d1", "d2"}) for _, key := range testKeys { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) + saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key)) } t.Run("keys with limit", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{Limit: 3}) { + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Limit: 3}) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"a1", "a2", "b1"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2", "b1"}), keys) }) t.Run("keys with range", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{StartKey: "b", EndKey: "d"}) { - require.NoError(t, err) - keys = append(keys, k) - } - assert.Equal(t, []string{"b1", "b2", "c1", "c2"}, keys) - }) - - t.Run("keys with prefix", func(t *testing.T) { - var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{ - StartKey: "c", - EndKey: resource.PrefixRangeEnd("c"), + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: namespacedKey(nsPrefix, "b"), + EndKey: namespacedKey(nsPrefix, "d"), }) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"c1", "c2"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"b1", "b2", "c1", "c2"}), keys) + }) + + t.Run("keys with prefix", func(t *testing.T) { + var keys []string + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: namespacedKey(nsPrefix, "c"), + EndKey: namespacedKey(nsPrefix, resource.PrefixRangeEnd("c")), + }) { + require.NoError(t, err) + keys = append(keys, k) + } + assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "c2"}), keys) }) t.Run("keys with limit and range", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{ - StartKey: "a", - EndKey: "c", + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: namespacedKey(nsPrefix, "a"), + EndKey: namespacedKey(nsPrefix, "c"), Limit: 2, }) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"a1", "a2"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2"}), keys) }) } func runTestKVKeysWithSort(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-keys-sort" + nsPrefix += "-keys-with-sort" // Setup test data - testKeys := []string{"a1", "a2", "b1", "b2", "c1"} + testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"}) for _, key := range testKeys { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) + saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key)) } t.Run("keys in ascending order (default)", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{Sort: resource.SortOrderAsc}) { + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Sort: resource.SortOrderAsc}) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"a1", "a2", "b1", "b2", "c1"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"}), keys) }) t.Run("keys in descending order", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{Sort: resource.SortOrderDesc}) { + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Sort: resource.SortOrderDesc}) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"c1", "b2", "b1", "a2", "a1"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "b2", "b1", "a2", "a1"}), keys) }) t.Run("keys descending with prefix", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{ - StartKey: "a", - EndKey: resource.PrefixRangeEnd("a"), + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ + StartKey: namespacedKey(nsPrefix, "a"), + EndKey: namespacedKey(nsPrefix, resource.PrefixRangeEnd("a")), Sort: resource.SortOrderDesc, }) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"a2", "a1"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"a2", "a1"}), keys) }) t.Run("keys descending with limit", func(t *testing.T) { var keys []string - for k, err := range kv.Keys(ctx, section, resource.ListOptions{ + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{ Sort: resource.SortOrderDesc, Limit: 3, }) { require.NoError(t, err) keys = append(keys, k) } - assert.Equal(t, []string{"c1", "b2", "b1"}, keys) + assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "b2", "b1"}), keys) }) } func runTestKVConcurrent(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(60*time.Second)) - section := nsPrefix + "-concurrent" + nsPrefix += "-concurrent" - t.Run("concurrent save and get operations", func(t *testing.T) { - const numGoroutines = 10 - const numOperations = 20 + // Test concurrent operations for both sections, as they have different behaviours + // in the sqlkv implementation. + for _, testSection := range []string{"unified/data", "unified/events"} { + t.Run(testSection, func(t *testing.T) { + t.Run("concurrent save and get operations", func(t *testing.T) { + const numGoroutines = 10 + const numOperations = 20 - done := make(chan error, numGoroutines) + done := make(chan error, numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - var err error - defer func() { done <- err }() + for goroutineID := range numGoroutines { + go func() { + var err error + defer func() { done <- err }() - for j := 0; j < numOperations; j++ { - key := fmt.Sprintf("concurrent-key-%d-%d", goroutineID, j) - value := fmt.Sprintf("concurrent-value-%d-%d", goroutineID, j) + for j := range numOperations { + key := namespacedKey(nsPrefix, fmt.Sprintf("concurrent-key-%d-%d", goroutineID, j)) + value := fmt.Sprintf("concurrent-value-%d-%d", goroutineID, j) - // Save - writer, err := kv.Save(ctx, section, key) - if err != nil { - return - } - defer func() { - err := writer.Close() - require.NoError(t, err) + // Save + writer, err := kv.Save(ctx, testSection, key) + if err != nil { + return + } + defer func() { + err := writer.Close() + require.NoError(t, err) + }() + _, err = io.Copy(writer, strings.NewReader(value)) + if err != nil { + return + } + err = writer.Close() + if err != nil { + return + } + + // Get immediately + reader, err := kv.Get(ctx, testSection, key) + if err != nil { + return + } + + readValue, err := io.ReadAll(reader) + require.NoError(t, err) + err = reader.Close() + require.NoError(t, err) + assert.Equal(t, value, string(readValue)) + } }() - _, err = io.Copy(writer, strings.NewReader(value)) - if err != nil { - return - } - err = writer.Close() - if err != nil { - return - } + } - // Get immediately - reader, err := kv.Get(ctx, section, key) - if err != nil { - return - } - - readValue, err := io.ReadAll(reader) + // Wait for all goroutines to complete + for range numGoroutines { + err := <-done require.NoError(t, err) - err = reader.Close() + } + }) + + t.Run("concurrent save, delete, and list operations", func(t *testing.T) { + const numGoroutines = 5 + done := make(chan error, numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + var err error + defer func() { done <- err }() + + key := namespacedKey(nsPrefix, fmt.Sprintf("concurrent-ops-key-%d", goroutineID)) + value := fmt.Sprintf("concurrent-ops-value-%d", goroutineID) + + // Save + writer, err := kv.Save(ctx, testSection, key) + if err != nil { + return + } + defer func() { + err := writer.Close() + require.NoError(t, err) + }() + _, err = io.Copy(writer, strings.NewReader(value)) + if err != nil { + return + } + err = writer.Close() + if err != nil { + return + } + + // List to verify it exists + found := false + for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) { + if err != nil { + return + } + if k == key { + found = true + break + } + } + if !found { + err = fmt.Errorf("key %s not found in list", key) + return + } + + // Delete + err = kv.Delete(ctx, testSection, key) + if err != nil { + return + } + + // Verify it's deleted + _, err = kv.Get(ctx, testSection, key) + require.ErrorIs(t, resource.ErrNotFound, err) + err = nil // Expected error, so clear it + }(i) + } + + // Wait for all goroutines to complete + for range numGoroutines { + err := <-done require.NoError(t, err) - assert.Equal(t, value, string(readValue)) } - }(i) - } - - // Wait for all goroutines to complete - for i := 0; i < numGoroutines; i++ { - err := <-done - require.NoError(t, err) - } - }) - - t.Run("concurrent save, delete, and list operations", func(t *testing.T) { - const numGoroutines = 5 - done := make(chan error, numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - var err error - defer func() { done <- err }() - - key := fmt.Sprintf("concurrent-ops-key-%d", goroutineID) - value := fmt.Sprintf("concurrent-ops-value-%d", goroutineID) - - // Save - writer, err := kv.Save(ctx, section, key) - if err != nil { - return - } - defer func() { - err := writer.Close() - require.NoError(t, err) - }() - _, err = io.Copy(writer, strings.NewReader(value)) - if err != nil { - return - } - err = writer.Close() - if err != nil { - return - } - - // List to verify it exists - found := false - for k, err := range kv.Keys(ctx, section, resource.ListOptions{}) { - if err != nil { - return - } - if k == key { - found = true - break - } - } - if !found { - err = fmt.Errorf("key %s not found in list", key) - return - } - - // Delete - err = kv.Delete(ctx, section, key) - if err != nil { - return - } - - // Verify it's deleted - _, err = kv.Get(ctx, section, key) - require.ErrorIs(t, resource.ErrNotFound, err) - err = nil // Expected error, so clear it - }(i) - } - - // Wait for all goroutines to complete - for i := 0; i < numGoroutines; i++ { - err := <-done - require.NoError(t, err) - } - }) + }) + }) + } } func runTestKVUnixTimestamp(t *testing.T, kv resource.KV, nsPrefix string) { @@ -533,29 +661,29 @@ func runTestKVUnixTimestamp(t *testing.T, kv resource.KV, nsPrefix string) { func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-batchget" + nsPrefix += "-batchget" t.Run("batch get existing keys", func(t *testing.T) { // Setup test data testData := map[string]string{ - "key1": "value1", - "key2": "value2", - "key3": "value3", + namespacedKey(nsPrefix, "key1"): "value1", + namespacedKey(nsPrefix, "key2"): "value2", + namespacedKey(nsPrefix, "key3"): "value3", } // Save test data for key, value := range testData { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value)) + saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader(value)) } // Batch get all keys - keys := []string{"key1", "key2", "key3"} + keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"}) type result struct { key string value string } var results []result - for kv, err := range kv.BatchGet(ctx, section, keys) { + for kv, err := range kv.BatchGet(ctx, testSection, keys) { require.NoError(t, err) value, err := io.ReadAll(kv.Value) require.NoError(t, err) @@ -565,10 +693,10 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { } // Verify results - assert.Len(t, results, 3) + require.Len(t, results, 3) // Check that all keys are present and in order - expectedKeys := []string{"key1", "key2", "key3"} + expectedKeys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"}) actualKeys := make([]string, len(results)) for i, r := range results { actualKeys[i] = r.key @@ -577,22 +705,40 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { // Verify values for _, r := range results { - assert.Equal(t, testData[r.key], r.value) + assert.Equal(t, testData[r.key], r.value, "key = %s", r.key) } }) + t.Run("batch get with empty section", func(t *testing.T) { + var kvs []resource.KeyValue + var errs []error + keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"}) + for kv, err := range kv.BatchGet(ctx, "", keys) { + if err != nil { + errs = append(errs, err) + continue + } + + kvs = append(kvs, kv) + } + + require.Len(t, errs, 1) + assert.Contains(t, errs[0].Error(), "section is required") + assert.Empty(t, kvs) + }) + t.Run("batch get with non-existent keys", func(t *testing.T) { // Setup some test data - saveKVHelper(t, kv, ctx, section, "existing-key", strings.NewReader("existing-value")) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "existing-key"), strings.NewReader("existing-value")) // Batch get with mix of existing and non-existent keys - keys := []string{"existing-key", "non-existent-1", "non-existent-2"} + keys := namespacedKeys(nsPrefix, []string{"existing-key", "non-existent-1", "non-existent-2"}) type result struct { key string value string } var results []result - for kv, err := range kv.BatchGet(ctx, section, keys) { + for kv, err := range kv.BatchGet(ctx, testSection, keys) { require.NoError(t, err) value, err := io.ReadAll(kv.Value) require.NoError(t, err) @@ -602,15 +748,15 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { } // Should only return the existing key - assert.Len(t, results, 1) - assert.Equal(t, "existing-key", results[0].key) + require.Len(t, results, 1) + assert.Equal(t, namespacedKey(nsPrefix, "existing-key"), results[0].key) assert.Equal(t, "existing-value", results[0].value) }) t.Run("batch get with all non-existent keys", func(t *testing.T) { - keys := []string{"non-existent-1", "non-existent-2", "non-existent-3"} + keys := namespacedKeys(nsPrefix, []string{"non-existent-1", "non-existent-2", "non-existent-3"}) var results []resource.KeyValue - for kv, err := range kv.BatchGet(ctx, section, keys) { + for kv, err := range kv.BatchGet(ctx, testSection, keys) { require.NoError(t, err) results = append(results, kv) } @@ -622,7 +768,7 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("batch get with empty keys list", func(t *testing.T) { keys := []string{} var results []resource.KeyValue - for kv, err := range kv.BatchGet(ctx, section, keys) { + for kv, err := range kv.BatchGet(ctx, testSection, keys) { require.NoError(t, err) results = append(results, kv) } @@ -632,16 +778,16 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { }) t.Run("batch get with empty section", func(t *testing.T) { - keys := []string{"some-key"} + keys := namespacedKeys(nsPrefix, []string{"some-key"}) var errors []error for kv, err := range kv.BatchGet(ctx, "", keys) { if err != nil { errors = append(errors, err) - break + continue } _ = kv // unused } - assert.Len(t, errors, 1) + require.Len(t, errors, 1) assert.Contains(t, errors[0].Error(), "section is required") }) @@ -655,13 +801,13 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { // Save test data for key, value := range testData { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value)) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, key), strings.NewReader(value)) } // Batch get in specific order - keys := []string{"z-key", "a-key", "m-key"} + keys := namespacedKeys(nsPrefix, []string{"z-key", "invalid-key1", "a-key", "invalid-key2", "m-key", "invalid-key3"}) var results []string - for kv, err := range kv.BatchGet(ctx, section, keys) { + for kv, err := range kv.BatchGet(ctx, testSection, keys) { require.NoError(t, err) err = kv.Value.Close() require.NoError(t, err) @@ -669,43 +815,43 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) { } // Verify order is preserved - assert.Len(t, results, 3) - expectedOrder := []string{"z-key", "a-key", "m-key"} + require.Len(t, results, 3) + expectedOrder := namespacedKeys(nsPrefix, []string{"z-key", "a-key", "m-key"}) assert.Equal(t, expectedOrder, results) }) } func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) - section := nsPrefix + "-batchdelete" + nsPrefix += "-batchdelete" t.Run("batch delete existing keys", func(t *testing.T) { // Setup test data testData := map[string]string{ - "key1": "value1", - "key2": "value2", - "key3": "value3", + namespacedKey(nsPrefix, "key1"): "value1", + namespacedKey(nsPrefix, "key2"): "value2", + namespacedKey(nsPrefix, "key3"): "value3", } // Save test data for key, value := range testData { - saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value)) + saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader(value)) } // Verify keys exist before deletion for key := range testData { - _, err := kv.Get(ctx, section, key) + _, err := kv.Get(ctx, testSection, key) require.NoError(t, err) } // Batch delete all keys - keys := []string{"key1", "key2", "key3"} - err := kv.BatchDelete(ctx, section, keys) + keys := slices.Collect(maps.Keys(testData)) + err := kv.BatchDelete(ctx, testSection, keys) require.NoError(t, err) // Verify all keys are deleted for _, key := range keys { - _, err := kv.Get(ctx, section, key) + _, err := kv.Get(ctx, testSection, key) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) } @@ -713,39 +859,40 @@ func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("batch delete with non-existent keys", func(t *testing.T) { // Setup some test data - saveKVHelper(t, kv, ctx, section, "existing-key-1", strings.NewReader("value1")) - saveKVHelper(t, kv, ctx, section, "existing-key-2", strings.NewReader("value2")) + key1, key2 := namespacedKey(nsPrefix, "existing-key-1"), namespacedKey(nsPrefix, "existing-key-2") + saveKVHelper(t, kv, ctx, testSection, key1, strings.NewReader("value1")) + saveKVHelper(t, kv, ctx, testSection, key2, strings.NewReader("value2")) // Batch delete with mix of existing and non-existent keys - keys := []string{"existing-key-1", "non-existent-1", "existing-key-2", "non-existent-2"} - err := kv.BatchDelete(ctx, section, keys) + keys := []string{key1, namespacedKey(nsPrefix, "non-existent-1"), key2, namespacedKey(nsPrefix, "non-existent-2")} + err := kv.BatchDelete(ctx, testSection, keys) require.NoError(t, err) // Verify existing keys are deleted - _, err = kv.Get(ctx, section, "existing-key-1") - assert.Error(t, err) + _, err = kv.Get(ctx, testSection, key1) + require.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) - _, err = kv.Get(ctx, section, "existing-key-2") - assert.Error(t, err) + _, err = kv.Get(ctx, testSection, key2) + require.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) }) t.Run("batch delete with all non-existent keys", func(t *testing.T) { // Batch delete keys that don't exist - keys := []string{"non-existent-1", "non-existent-2", "non-existent-3"} - err := kv.BatchDelete(ctx, section, keys) + keys := namespacedKeys(nsPrefix, []string{"non-existent-1", "non-existent-2", "non-existent-3"}) + err := kv.BatchDelete(ctx, testSection, keys) require.NoError(t, err) }) t.Run("batch delete with empty keys list", func(t *testing.T) { keys := []string{} - err := kv.BatchDelete(ctx, section, keys) + err := kv.BatchDelete(ctx, testSection, keys) require.NoError(t, err) }) t.Run("batch delete with empty section", func(t *testing.T) { - keys := []string{"some-key"} + keys := namespacedKeys(nsPrefix, []string{"some-key"}) err := kv.BatchDelete(ctx, "", keys) assert.Error(t, err) assert.Contains(t, err.Error(), "section is required") @@ -753,27 +900,27 @@ func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("batch delete preserves other keys", func(t *testing.T) { // Setup test data - saveKVHelper(t, kv, ctx, section, "keep-key-1", strings.NewReader("keep-value-1")) - saveKVHelper(t, kv, ctx, section, "delete-key-1", strings.NewReader("delete-value-1")) - saveKVHelper(t, kv, ctx, section, "keep-key-2", strings.NewReader("keep-value-2")) - saveKVHelper(t, kv, ctx, section, "delete-key-2", strings.NewReader("delete-value-2")) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "keep-key-1"), strings.NewReader("keep-value-1")) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "delete-key-1"), strings.NewReader("delete-value-1")) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "keep-key-2"), strings.NewReader("keep-value-2")) + saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "delete-key-2"), strings.NewReader("delete-value-2")) // Batch delete specific keys - keys := []string{"delete-key-1", "delete-key-2"} - err := kv.BatchDelete(ctx, section, keys) + keys := namespacedKeys(nsPrefix, []string{"delete-key-1", "delete-key-2"}) + err := kv.BatchDelete(ctx, testSection, keys) require.NoError(t, err) // Verify deleted keys are gone - _, err = kv.Get(ctx, section, "delete-key-1") + _, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "delete-key-1")) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) - _, err = kv.Get(ctx, section, "delete-key-2") + _, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "delete-key-2")) assert.Error(t, err) assert.Equal(t, resource.ErrNotFound, err) // Verify kept keys still exist - reader, err := kv.Get(ctx, section, "keep-key-1") + reader, err := kv.Get(ctx, testSection, namespacedKey(nsPrefix, "keep-key-1")) require.NoError(t, err) value, err := io.ReadAll(reader) require.NoError(t, err) @@ -781,7 +928,7 @@ func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) { err = reader.Close() require.NoError(t, err) - reader, err = kv.Get(ctx, section, "keep-key-2") + reader, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "keep-key-2")) require.NoError(t, err) value, err = io.ReadAll(reader) require.NoError(t, err) @@ -794,6 +941,7 @@ func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) { // saveKVHelper is a helper function to save data to KV store using the new WriteCloser interface func saveKVHelper(t *testing.T, kv resource.KV, ctx context.Context, section, key string, value io.Reader) { t.Helper() + writer, err := kv.Save(ctx, section, key) require.NoError(t, err) _, err = io.Copy(writer, value) diff --git a/pkg/storage/unified/testing/kv_test.go b/pkg/storage/unified/testing/kv_test.go index 1e9b1a16c45..af7de65e52c 100644 --- a/pkg/storage/unified/testing/kv_test.go +++ b/pkg/storage/unified/testing/kv_test.go @@ -7,7 +7,11 @@ import ( badger "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" ) func TestBadgerKV(t *testing.T) { @@ -26,3 +30,19 @@ func TestBadgerKV(t *testing.T) { NSPrefix: "badger-kv-test", }) } + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestSQLKV(t *testing.T) { + RunKVTest(t, func(ctx context.Context) resource.KV { + dbstore := db.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + + kv, err := resource.NewSQLKV(eDB) + require.NoError(t, err) + return kv + }, &KVTestOptions{NSPrefix: "sql-kv-test"}) +} diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index ce36d8836bc..730efe418f6 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/bwmarrin/snowflake" "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -25,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -42,10 +44,14 @@ const ( TestCreateNewResource = "create new resource" TestGetResourceLastImportTime = "get resource last import time" TestOptimisticLocking = "optimistic locking on concurrent writes" + TestKeyPathGeneration = "key_path generation" ) type NewBackendFunc func(ctx context.Context) resource.StorageBackend +// NewBackendWithDBFunc creates a backend with database access for testing +type NewBackendWithDBFunc func(ctx context.Context) (resource.StorageBackend, sqldb.DB) + // TestOptions configures which tests to run type TestOptions struct { SkipTests map[string]bool // tests to skip @@ -100,6 +106,37 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp } } +func RunSQLStorageBackendCompatibilityTest(t *testing.T, newBackend NewBackendWithDBFunc, opts *TestOptions) { + if opts == nil { + opts = &TestOptions{} + } + + if opts.NSPrefix == "" { + opts.NSPrefix = GenerateRandomNSPrefix() + } + + t.Logf("Running tests with namespace prefix: %s", opts.NSPrefix) + + cases := []struct { + name string + fn func(*testing.T, resource.StorageBackend, string, sqldb.DB) + }{ + {TestKeyPathGeneration, runTestIntegrationBackendKeyPathGeneration}, + } + + for _, tc := range cases { + if shouldSkip := opts.SkipTests[tc.name]; shouldSkip { + t.Logf("Skipping test: %s", tc.name) + continue + } + + t.Run(tc.name, func(t *testing.T) { + backend, db := newBackend(context.Background()) + tc.fn(t, backend, opts.NSPrefix, db) + }) + } +} + func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ @@ -1722,3 +1759,222 @@ func runTestIntegrationBackendOptimisticLocking(t *testing.T, backend resource.S require.LessOrEqual(t, successes, 1, "at most one create should succeed (errors: %v)", errorMessages) }) } + +func runTestIntegrationBackendKeyPathGeneration(t *testing.T, backend resource.StorageBackend, nsPrefix string, db sqldb.DB) { + ctx := testutil.NewDefaultTestContext(t) + + t.Run("Create resource", func(t *testing.T) { + // Create a test resource + key := &resourcepb.ResourceKey{ + Group: "playlist.grafana.app", + Resource: "playlists", + Namespace: nsPrefix + "-default", + Name: "test-playlist-crud", + } + + // Create the K8s unstructured object + testObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "playlist.grafana.app/v0alpha1", + "kind": "Playlist", + "metadata": map[string]interface{}{ + "name": "test-playlist-crud", + "namespace": nsPrefix + "-default", + "uid": "test-uid-crud-123", + }, + "spec": map[string]interface{}{ + "title": "My Test Playlist", + }, + }, + } + + // Get metadata accessor + metaAccessor, err := utils.MetaAccessor(testObj) + require.NoError(t, err) + + // Serialize to JSON + jsonBytes, err := testObj.MarshalJSON() + require.NoError(t, err) + + // Create WriteEvent + writeEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: key, + Value: jsonBytes, + Object: metaAccessor, + PreviousRV: 0, // Always 0 for new resources + GUID: "create-guid-crud-123", + } + + // Create the resource using WriteEvent + createRV, err := backend.WriteEvent(ctx, writeEvent) + require.NoError(t, err) + require.Greater(t, createRV, int64(0)) + + // Verify created resource key_path + verifyKeyPath(t, db, ctx, key, "created", createRV, "") + + t.Run("Update resource", func(t *testing.T) { + // Update the resource + testObj.Object["spec"] = map[string]interface{}{ + "title": "My Updated Playlist", + } + + updatedMetaAccessor, err := utils.MetaAccessor(testObj) + require.NoError(t, err) + + updatedJsonBytes, err := testObj.MarshalJSON() + require.NoError(t, err) + + updateEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_MODIFIED, + Key: key, + Value: updatedJsonBytes, + Object: updatedMetaAccessor, + PreviousRV: createRV, + GUID: fmt.Sprintf("update-guid-%d", createRV), + } + + // Update the resource + updateRV, err := backend.WriteEvent(ctx, updateEvent) + require.NoError(t, err) + require.Greater(t, updateRV, createRV) + + // Verify updated resource key_path + verifyKeyPath(t, db, ctx, key, "updated", updateRV, "") + + t.Run("Delete resource", func(t *testing.T) { + deleteEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_DELETED, + Key: key, + Value: updatedJsonBytes, // Keep the last known value + Object: updatedMetaAccessor, + PreviousRV: updateRV, + GUID: fmt.Sprintf("delete-guid-%d", updateRV), + } + + // Delete the resource + deleteRV, err := backend.WriteEvent(ctx, deleteEvent) + require.NoError(t, err) + require.Greater(t, deleteRV, updateRV) + + // Verify deleted resource key_path + verifyKeyPath(t, db, ctx, key, "deleted", deleteRV, "") + }) + }) + }) + + t.Run("Resource with folder", func(t *testing.T) { + // Create a resource in a folder + folderKey := &resourcepb.ResourceKey{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Namespace: nsPrefix + "-default", + Name: "my-dashboard", + } + + // Create dashboard object with folder + dashboardObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v0alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "my-dashboard", + "namespace": nsPrefix + "-default", + "uid": "dash-uid-456", + "annotations": map[string]interface{}{ + "grafana.app/folder": "test-folder", + }, + }, + "spec": map[string]interface{}{ + "title": "My Dashboard", + }, + }, + } + + folderMetaAccessor, err := utils.MetaAccessor(dashboardObj) + require.NoError(t, err) + + folderJsonBytes, err := dashboardObj.MarshalJSON() + require.NoError(t, err) + + folderWriteEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: folderKey, + Value: folderJsonBytes, + Object: folderMetaAccessor, + PreviousRV: 0, + GUID: "folder-guid-456", + } + + // Create the dashboard in folder + folderRV, err := backend.WriteEvent(ctx, folderWriteEvent) + require.NoError(t, err) + require.Greater(t, folderRV, int64(0)) + + // Verify folder resource key_path includes folder + verifyKeyPath(t, db, ctx, folderKey, "created", folderRV, "test-folder") + }) +} + +// verifyKeyPath is a helper function to verify key_path generation +func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) { + var query string + if db.DriverName() == "postgres" { + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3" + } else { + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?" + } + rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion) + require.NoError(t, err) + + require.True(t, rows.Next()) + + var keyPath string + var actualRV int64 + var actualAction int + var actualFolder string + + err = rows.Scan(&keyPath, &actualRV, &actualAction, &actualFolder) + require.NoError(t, err) + err = rows.Close() + require.NoError(t, err) + + // Verify basic key_path format + require.Contains(t, keyPath, "unified/data/") + require.Contains(t, keyPath, key.Group) + require.Contains(t, keyPath, key.Resource) + require.Contains(t, keyPath, key.Namespace) + require.Contains(t, keyPath, key.Name) + + // Verify action suffix + require.Contains(t, keyPath, fmt.Sprintf("~%s~", action)) + + // Verify snowflake calculation + expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000) + require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), fmt.Sprintf("actual RV: %d", actualRV)) + + // Verify folder if specified + if expectedFolder != "" { + require.Equal(t, expectedFolder, actualFolder) + require.Contains(t, keyPath, expectedFolder) + } + + // Verify action code matches + var expectedActionCode int + switch action { + case "created": + expectedActionCode = 1 + case "updated": + expectedActionCode = 2 + case "deleted": + expectedActionCode = 3 + } + require.Equal(t, expectedActionCode, actualAction) + + t.Logf("Action: %s, RV: %d, Snowflake: %d", action, resourceVersion, expectedSnowflake) + t.Logf("Key_path: %s", keyPath) + if expectedFolder != "" { + t.Logf("Folder: %s", actualFolder) + } +} diff --git a/pkg/storage/unified/testing/storage_backend_test.go b/pkg/storage/unified/testing/storage_backend_test.go index 04f34e9102f..70e3b15aa7b 100644 --- a/pkg/storage/unified/testing/storage_backend_test.go +++ b/pkg/storage/unified/testing/storage_backend_test.go @@ -7,7 +7,11 @@ import ( badger "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" ) func TestBadgerKVStorageBackend(t *testing.T) { @@ -25,7 +29,7 @@ func TestBadgerKVStorageBackend(t *testing.T) { require.NoError(t, err) return backend }, &TestOptions{ - NSPrefix: "kvstorage-test", + NSPrefix: "badgerkvstorage-test", SkipTests: map[string]bool{ // TODO: fix these tests and remove this skip TestBlobSupport: true, @@ -35,3 +39,50 @@ func TestBadgerKVStorageBackend(t *testing.T) { }, }) } + +func TestSQLKVStorageBackend(t *testing.T) { + newBackendFunc := func(ctx context.Context) (resource.StorageBackend, sqldb.DB) { + dbstore := db.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + kv, err := resource.NewSQLKV(eDB) + require.NoError(t, err) + kvOpts := resource.KVBackendOptions{ + KvStore: kv, + } + backend, err := resource.NewKVStorageBackend(kvOpts) + require.NoError(t, err) + db, err := eDB.Init(ctx) + require.NoError(t, err) + return backend, db + } + + RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + backend, _ := newBackendFunc(ctx) + return backend + }, &TestOptions{ + NSPrefix: "sqlkvstorage-test", + SkipTests: map[string]bool{ + TestHappyPath: true, + TestWatchWriteEvents: true, + TestList: true, + TestBlobSupport: true, + TestGetResourceStats: true, + TestListHistory: true, + TestListHistoryErrorReporting: true, + TestListModifiedSince: true, + TestListTrash: true, + TestCreateNewResource: true, + TestGetResourceLastImportTime: true, + TestOptimisticLocking: true, + TestKeyPathGeneration: true, + }, + }) + + RunSQLStorageBackendCompatibilityTest(t, newBackendFunc, &TestOptions{ + NSPrefix: "sqlkvstorage-compatibility-test", + SkipTests: map[string]bool{ + TestKeyPathGeneration: true, + }, + }) +} diff --git a/pkg/tests/api/alerting/api_backtesting_test.go b/pkg/tests/api/alerting/api_backtesting_test.go index f07be49ff01..faf50a60da2 100644 --- a/pkg/tests/api/alerting/api_backtesting_test.go +++ b/pkg/tests/api/alerting/api_backtesting_test.go @@ -68,7 +68,7 @@ func TestBacktesting(t *testing.T) { require.Truef(t, ok, "The data file does not contain a field `data`") status, body := apiCli.SubmitRuleForBacktesting(t, request) - require.Equal(t, http.StatusOK, status) + require.Equalf(t, http.StatusOK, status, "Response: %s", body) var result data.Frame require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") }) @@ -107,6 +107,7 @@ func TestBacktesting(t *testing.T) { resourcepermissions.SetResourcePermissionCommand{ Actions: []string{ accesscontrol.ActionAlertingRuleRead, + accesscontrol.ActionAlertingRuleUpdate, }, Resource: "folders", ResourceID: "*", diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 4fc9151602f..f33ce55d851 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -1166,11 +1166,12 @@ func TestIntegrationConvertPrometheusEndpoints_Editor(t *testing.T) { testinfra.SQLiteIntegrationTest(t) dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableRecordingRules: true, + DisableAuthZClientCache: true, + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableRecordingRules: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index b35bf6959c5..ea6fa972f97 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2470,7 +2470,7 @@ var expNonEmailNotifications = map[string][]string{ "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert1/view?orgId=1", "text": "Integration Test ", "fallback": "Integration Test [FIRING:1] SlackAlert1 (default)", - "footer": "Grafana v", + "footer": "Grafana", "footer_icon": "https://grafana.com/static/assets/img/fav32.png", "color": "#D63232", "ts": %s, @@ -2490,7 +2490,7 @@ var expNonEmailNotifications = map[string][]string{ "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert2/view?orgId=1", "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SlackAlert2\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SlackAlert2/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%%3DUID_SlackAlert2&orgId=1\n", "fallback": "[FIRING:1] SlackAlert2 (default)", - "footer": "Grafana v", + "footer": "Grafana", "footer_icon": "https://grafana.com/static/assets/img/fav32.png", "color": "#D63232", "ts": %s, diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 61bc3195857..067372a3470 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sort" "testing" "time" @@ -364,8 +365,6 @@ func TestIntegrationPrometheusRules(t *testing.T) { func TestIntegrationPrometheusRulesPagination(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) - testinfra.SQLiteIntegrationTest(t) - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, @@ -388,23 +387,30 @@ func TestIntegrationPrometheusRulesPagination(t *testing.T) { require.NoError(t, err) // Create 3 rule groups with different numbers of rules - // Group 1: 5 rules, Group 2: 3 rules, Group 3: 2 rules (total: 10 rules) + // Group 1: 5 rules with team=backend + // Group 2: 3 rules with team=frontend + // Group 3: 2 rules with team=platform for groupIdx := 1; groupIdx <= 3; groupIdx++ { var rulesCount int + var team string switch groupIdx { case 1: rulesCount = 5 + team = "backend" case 2: rulesCount = 3 + team = "frontend" case 3: rulesCount = 2 + team = "platform" } rules := make([]apimodels.PostableExtendedRuleNode, rulesCount) for i := 0; i < rulesCount; i++ { rules[i] = apimodels.PostableExtendedRuleNode{ ApiRuleNode: &apimodels.ApiRuleNode{ - For: &interval, + For: &interval, + Labels: map[string]string{"team": team}, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ Title: fmt.Sprintf("rule-%d-%d", groupIdx, i+1), @@ -514,6 +520,61 @@ func TestIntegrationPrometheusRulesPagination(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) require.Len(t, result.Data.RuleGroups, 0, "should return no groups") }) + + t.Run("with rule_matcher filter returns only matching rules", func(t *testing.T) { + matcher := url.QueryEscape(`{"name":"team","value":"frontend","isRegex":false,"isEqual":true}`) + promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?rule_matcher=%s", grafanaListedAddr, matcher) + // nolint:gosec + resp, err := http.Get(promRulesURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + + var result apimodels.RuleResponse + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Should only return group-2 (team=frontend, 3 rules) + foundGroups := []string{} + total := 0 + for _, group := range result.Data.RuleGroups { + foundGroups = append(foundGroups, group.Name) + total += len(group.Rules) + } + require.Equal(t, []string{"group-2"}, foundGroups) + require.Equal(t, 3, total) + }) + + t.Run("with rule_matcher regex filter", func(t *testing.T) { + // Filter with regex team=~plat.* (should match group-3 with team=platform) + matcher := url.QueryEscape(`{"name":"team","value":"plat.*","isRegex":true,"isEqual":true}`) + promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?rule_matcher=%s", grafanaListedAddr, matcher) + // nolint:gosec + resp, err := http.Get(promRulesURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + + var result apimodels.RuleResponse + err = json.NewDecoder(resp.Body).Decode(&result) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Should only return group-3 (team=platform matches plat.*) + foundGroups := []string{} + total := 0 + for _, group := range result.Data.RuleGroups { + foundGroups = append(foundGroups, group.Name) + total += len(group.Rules) + } + require.Equal(t, []string{"group-3"}, foundGroups) + require.Equal(t, 2, total) + }) } func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 20484a38bf4..e952740f2e6 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -1192,10 +1192,11 @@ func TestIntegrationExportFileProvisionContactPoints(t *testing.T) { func TestIntegrationFullpath(t *testing.T) { dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, + DisableAuthZClientCache: true, + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 9e8855d8f19..a05829976f4 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -53,10 +53,11 @@ func TestIntegrationAlertRulePermissions(t *testing.T) { testinfra.SQLiteIntegrationTest(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, + DisableAuthZClientCache: true, + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) @@ -360,10 +361,11 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) { testinfra.SQLiteIntegrationTest(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, + DisableAuthZClientCache: true, + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) @@ -1429,10 +1431,11 @@ func TestIntegrationRuleGroupSequence(t *testing.T) { testinfra.SQLiteIntegrationTest(t) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, + DisableAuthZClientCache: true, + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) diff --git a/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json b/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json index 7bf5ff10535..fe4f2f2f924 100644 --- a/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json +++ b/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json @@ -82,6 +82,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -208,6 +209,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -352,6 +354,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -451,6 +454,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -748,6 +752,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -910,6 +915,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -1194,6 +1200,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -1392,6 +1399,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -1793,6 +1801,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -1820,6 +1829,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2318,6 +2328,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2610,6 +2621,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "token", @@ -2628,6 +2640,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2686,6 +2699,24 @@ "secure": false, "dependsOn": "", "subformOptions": null + }, + { + "element": "input", + "inputType": "text", + "label": "Footer", + "description": "Templated footer of the slack message", + "placeholder": "{{ template \"slack.default.footer\" . }}", + "propertyName": "footer", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null } ] }, @@ -2953,6 +2984,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -3303,6 +3335,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -3393,6 +3426,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -3474,6 +3508,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -3916,6 +3951,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -4114,6 +4150,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -4201,6 +4238,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "secret", diff --git a/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json b/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json index d942144ef9c..d3797d9cafa 100644 --- a/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json +++ b/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json @@ -93,6 +93,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -225,6 +226,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -1300,6 +1302,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -1405,6 +1408,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2476,6 +2480,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2645,6 +2650,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -2935,6 +2941,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -3139,6 +3146,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -4405,6 +4413,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -5334,6 +5343,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -6630,6 +6640,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -6928,6 +6939,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "token", @@ -6946,6 +6958,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -7004,6 +7017,24 @@ "secure": false, "dependsOn": "", "subformOptions": null + }, + { + "element": "input", + "inputType": "text", + "label": "Footer", + "description": "Templated footer of the slack message", + "placeholder": "{{ template \"slack.default.footer\" . }}", + "propertyName": "footer", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null } ] }, @@ -9237,6 +9268,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -11515,6 +11547,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "", @@ -12308,6 +12341,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -13001,6 +13035,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -13443,6 +13478,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -13641,6 +13677,7 @@ "is": "" }, "required": false, + "protected": true, "validationRule": "", "secure": false, "dependsOn": "", @@ -15072,6 +15109,7 @@ "is": "" }, "required": true, + "protected": true, "validationRule": "", "secure": true, "dependsOn": "secret", diff --git a/pkg/tests/api/alerting/test-data/api_backtesting_data.json b/pkg/tests/api/alerting/test-data/api_backtesting_data.json index d02b6905f0b..5fe0f621126 100644 --- a/pkg/tests/api/alerting/test-data/api_backtesting_data.json +++ b/pkg/tests/api/alerting/test-data/api_backtesting_data.json @@ -12,6 +12,9 @@ }, "condition": "A", "no_data_state": "Alerting", + "title": "test-rule-backtesting-data", + "rule_group": "test-group", + "namespace_uid": "test-namespace", "data": [ { "refId": "A", @@ -193,6 +196,9 @@ }, "condition": "C", "no_data_state": "Alerting", + "title": "test-rule-backtesting-data", + "rule_group": "test-group", + "namespace_uid": "test-namespace", "data": [ { "refId": "A", diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json index 18d8b8cea40..f8c01a3d883 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json @@ -72,6 +72,33 @@ }, "isPaused": false, "missing_series_evals_to_resolve": 2 + }, + { + "uid": "", + "title": "RecordingRule1", + "data": [ + { + "refId": "A", + "relativeTimeRange": { + "from": 0, + "to": 0 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "1 + 1", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "A", + "type": "math" + } + } + ], + "isPaused": false, + "record": { + "metric": "test_metric", + "from": "A", + "targetDatasourceUid": "test-datasource-uid" + } } ] } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-get.json b/pkg/tests/api/alerting/test-data/rulegroup-1-get.json index 333bd32c0d1..d392254d756 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-get.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-get.json @@ -97,6 +97,44 @@ }, "missing_series_evals_to_resolve": 2 } + }, + { + "expr": "", + "for": "0s", + "keep_firing_for": "0s", + "grafana_alert": { + "title": "RecordingRule1", + "data": [ + { + "refId": "A", + "queryType": "", + "relativeTimeRange": { + "from": 0, + "to": 0 + }, + "datasourceUid": "__expr__", + "model": { + "expression": "1 + 1", + "intervalMs": 1000, + "maxDataPoints": 43200, + "type": "math" + } + } + ], + "updated": "2023-09-29T17:37:19Z", + "intervalSeconds": 60, + "version": 1, + "uid": "", + "namespace_uid": "", + "rule_group": "Group1", + "is_paused": false, + "record": { + "metric": "test_metric", + "from": "A", + "target_datasource_uid": "test-datasource-uid" + }, + "metadata": {} + } } ] } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-post.json b/pkg/tests/api/alerting/test-data/rulegroup-1-post.json index f9f1441eb18..e020299fc42 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-post.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-post.json @@ -53,6 +53,26 @@ "exec_err_state": "Alerting", "missing_series_evals_to_resolve": 2 } + }, + { + "grafana_alert": { + "title": "RecordingRule1", + "data": [ + { + "refId": "A", + "datasourceUid": "__expr__", + "model": { + "expression": "1 + 1", + "type": "math" + } + } + ], + "record": { + "metric": "test_metric", + "from": "A", + "target_datasource_uid": "test-datasource-uid" + } + } } ] } diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index b49e61d3a4e..3bae37f209a 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -249,6 +249,7 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul ExecErrState: gettable.ExecErrState, IsPaused: &gettable.IsPaused, NotificationSettings: gettable.NotificationSettings, + Record: gettable.Record, Metadata: gettable.Metadata, } } diff --git a/pkg/tests/api/annotations/annotations_test.go b/pkg/tests/api/annotations/annotations_test.go index 89e7131d4b5..6391d37d762 100644 --- a/pkg/tests/api/annotations/annotations_test.go +++ b/pkg/tests/api/annotations/annotations_test.go @@ -32,8 +32,9 @@ func TestIntegrationAnnotations(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - EnableFeatureToggles: []string{featuremgmt.FlagAnnotationPermissionUpdate}, + DisableAuthZClientCache: true, + DisableAnonymous: true, + EnableFeatureToggles: []string{featuremgmt.FlagAnnotationPermissionUpdate}, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) noneUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 50ab7d939ea..84ae8c62c62 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,8 +39,15 @@ func TestMain(m *testing.M) { func TestIntegrationDashboardServiceValidation(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) + unifiedConfig := make(map[string]setting.UnifiedStorageConfig) + for _, resource := range []string{"folders.folder.grafana.app", "dashboards.dashboard.grafana.app"} { + unifiedConfig[resource] = setting.UnifiedStorageConfig{ + EnableMigration: true, + } + } dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableAnonymous: true, + UnifiedStorageConfig: unifiedConfig, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) @@ -218,11 +226,12 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { require.NoError(t, err) }) - t.Run("When updating uid with id", func(t *testing.T) { + dashboardWithDuplicatedLegacyAnnotation := "new-uid" + t.Run("When saving a dashboard with an already used legacy ID", func(t *testing.T) { resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ "dashboard": map[string]interface{}{ "id": savedDashInFolder.ID, // nolint:staticcheck - "uid": "new-uid", + "uid": dashboardWithDuplicatedLegacyAnnotation, "title": "Updated title", }, "folderUid": savedDashInFolder.FolderUID, @@ -233,7 +242,49 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) - t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) { + + t.Run("When updating a dashboard with legacy ID in multiple dashboards", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, // nolint:staticcheck + "uid": savedDashInGeneralFolder.UID, + "title": "Updated title", + }, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + // Delete the dashboard with duplicated legacy ID annotation + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s", grafanaListedAddr, dashboardWithDuplicatedLegacyAnnotation) + req, err := http.NewRequest("DELETE", u, nil) + require.NoError(t, err) + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + err = resp.Body.Close() + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("When updating a dashboard already using that uid", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, + "uid": savedDashInFolder.UID, + "title": "Dashboard with existing UID", + }, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When updating id with a dashboard already using that uid", func(t *testing.T) { resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ "dashboard": map[string]interface{}{ "id": savedDashInFolder.ID, // nolint:staticcheck @@ -266,6 +317,31 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + + // Obs: in legacy, the dashboard request would fail + // After the dashboard is created, the user can see that there is an error with the library panel and can remove them manually + t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Bad dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "gridPos": map[string]int{"h": 0, "w": 0, "x": 0, "y": 0}, + "libraryPanel": map[string]string{ + "name": "Bad panel", + "uid": "invalid-uid", + }, + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + _, err = io.ReadAll(resp.Body) + require.NoError(t, err) + err = resp.Body.Close() + require.NoError(t, err) + }) } func TestIntegrationDashboardQuota(t *testing.T) { @@ -303,7 +379,7 @@ func TestIntegrationDashboardQuota(t *testing.T) { dashboardDTO := &plugindashboards.PluginDashboard{} err = json.Unmarshal(b, dashboardDTO) require.NoError(t, err) - require.EqualValues(t, 1, dashboardDTO.DashboardId) + require.EqualValues(t, "just testing", dashboardDTO.Title) }) t.Run("when quota limit exceeds importing a dashboard should fail", func(t *testing.T) { @@ -392,7 +468,7 @@ providers: dashboardUID = d.UID dashboardID = d.ID // nolint:staticcheck } - assert.Equal(t, int64(1), dashboardID) + assert.Len(t, *dashboardList, 1) testCases := []struct { desc string @@ -752,7 +828,7 @@ func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) { }, { "id": 2, - "title": "Library Panel 2", + "title": "Library Panel 2", "type": "stat", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, "libraryPanel": { @@ -776,7 +852,7 @@ func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) { } }, "test-lib-panel-2": { - "uid": "test-lib-panel-2", + "uid": "test-lib-panel-2", "name": "Test Library Panel 2", "kind": 1, "type": "stat", @@ -986,7 +1062,8 @@ func TestIntegrationDashboardServicePermissions(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableAnonymous: true, + DisableAuthZClientCache: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 0f7b476ecad..5497c8f1685 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -143,7 +143,7 @@ func TestIntegrationResourcePermissions(t *testing.T) { adminClient := test_common.NewReceiverClient(t, admin) writeACMetadata := []string{"canWrite", "canDelete"} - allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"} + allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin", "canModifyProtected"} mustID := func(user apis.User) int64 { id, err := user.Identity.GetInternalID() @@ -404,13 +404,14 @@ func TestIntegrationAccessControl(t *testing.T) { org1 := helper.Org1 type testCase struct { - user apis.User - canRead bool - canUpdate bool - canCreate bool - canDelete bool - canReadSecrets bool - canAdmin bool + user apis.User + canRead bool + canUpdate bool + canUpdateProtected bool + canCreate bool + canDelete bool + canReadSecrets bool + canAdmin bool } // region users unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{}) @@ -473,20 +474,22 @@ func TestIntegrationAccessControl(t *testing.T) { testCases := []testCase{ { - user: unauthorized, - canRead: false, - canUpdate: false, - canCreate: false, - canDelete: false, + user: unauthorized, + canRead: false, + canUpdate: false, + canUpdateProtected: false, + canCreate: false, + canDelete: false, }, { - user: org1.Admin, - canRead: true, - canCreate: true, - canUpdate: true, - canDelete: true, - canAdmin: true, - canReadSecrets: true, + user: org1.Admin, + canRead: true, + canCreate: true, + canUpdate: true, + canUpdateProtected: true, + canDelete: true, + canAdmin: true, + canReadSecrets: true, }, { user: org1.Editor, @@ -535,22 +538,24 @@ func TestIntegrationAccessControl(t *testing.T) { canDelete: true, }, { - user: adminLikeUser, - canRead: true, - canCreate: true, - canUpdate: true, - canDelete: true, - canAdmin: true, - canReadSecrets: true, + user: adminLikeUser, + canRead: true, + canCreate: true, + canUpdate: true, + canUpdateProtected: true, + canDelete: true, + canAdmin: true, + canReadSecrets: true, }, { - user: adminLikeUserLongName, - canRead: true, - canCreate: true, - canUpdate: true, - canDelete: true, - canAdmin: true, - canReadSecrets: true, + user: adminLikeUserLongName, + canRead: true, + canCreate: true, + canUpdate: true, + canUpdateProtected: true, + canDelete: true, + canAdmin: true, + canReadSecrets: true, }, } @@ -609,6 +614,9 @@ func TestIntegrationAccessControl(t *testing.T) { if tc.canUpdate { expectedWithMetadata.SetAccessControl("canWrite") } + if tc.canUpdateProtected { + expectedWithMetadata.SetAccessControl("canModifyProtected") + } if tc.canDelete { expectedWithMetadata.SetAccessControl("canDelete") } @@ -672,6 +680,32 @@ func TestIntegrationAccessControl(t *testing.T) { require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) }) }) + + updatedExpected = expected.Copy().(*v0alpha1.Receiver) + updatedExpected.Spec.Integrations = []v0alpha1.ReceiverIntegration{ + createIntegration(t, "webhook"), + } + + expected, err = adminClient.Update(ctx, updatedExpected, v1.UpdateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.NotNil(t, expected) + + updatedProtected := expected.Copy().(*v0alpha1.Receiver) + updatedProtected.Spec.Integrations[0].Settings["url"] = "http://localhost:8080/webhook" + + if tc.canUpdateProtected { + t.Run("should be able to update protected fields of the receiver", func(t *testing.T) { + updated, err := client.Update(ctx, updatedProtected, v1.UpdateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.NotNil(t, updated) + expected = updated + }) + } else { + t.Run("should be forbidden to edit protected fields of the receiver", func(t *testing.T) { + _, err := client.Update(ctx, updatedProtected, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + } } else { t.Run("should be forbidden to update receiver", func(t *testing.T) { _, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) @@ -684,6 +718,7 @@ func TestIntegrationAccessControl(t *testing.T) { require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) }) }) + require.Falsef(t, tc.canUpdateProtected, "Invalid combination of assertions. CanUpdateProtected should be false") } deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}} @@ -1291,6 +1326,7 @@ func TestIntegrationCRUD(t *testing.T) { receiver.SetAccessControl("canDelete") receiver.SetAccessControl("canReadSecrets") receiver.SetAccessControl("canAdmin") + receiver.SetAccessControl("canModifyProtected") receiver.SetInUse(0, nil) receiver.SetCanUse(true) diff --git a/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json index 621f9c3af16..f092d8980f7 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json +++ b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json @@ -8,6 +8,7 @@ "annotations": { "grafana.com/access/canAdmin": "true", "grafana.com/access/canDelete": "true", + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/access/canWrite": "true", "grafana.com/canUse": "true", @@ -40,6 +41,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -61,6 +63,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -105,6 +108,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -153,6 +157,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -211,6 +216,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -256,6 +262,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -317,6 +324,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "1", @@ -388,6 +396,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -441,6 +450,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -525,6 +535,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -579,6 +590,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -625,6 +637,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -674,6 +687,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", @@ -722,6 +736,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "1", @@ -767,6 +782,7 @@ "kind": "Receiver", "metadata": { "annotations": { + "grafana.com/access/canModifyProtected": "true", "grafana.com/access/canReadSecrets": "true", "grafana.com/canUse": "false", "grafana.com/inUse/routes": "0", diff --git a/pkg/tests/apis/alerting/notifications/templategroup/imported_test.go b/pkg/tests/apis/alerting/notifications/templategroup/imported_test.go new file mode 100644 index 00000000000..9a3e88f3e46 --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/templategroup/imported_test.go @@ -0,0 +1,129 @@ +package templateGroup + +import ( + "context" + "embed" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" + "github.com/grafana/grafana/pkg/services/featuremgmt" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/tests/api/alerting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +//go:embed test-data/*.* +var testData embed.FS + +func TestIntegrationImportedTemplates(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{ + featuremgmt.FlagAlertingImportAlertmanagerAPI, + }, + }) + + client := common.NewTemplateGroupClient(t, helper.Org1.Admin) + + cliCfg := helper.Org1.Admin.NewRestConfig() + alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) + + configYaml, err := testData.ReadFile(path.Join("test-data", "imported.yaml")) + require.NoError(t, err) + + identifier := "test-create-get-config" + mergeMatchers := "_imported=true" + + headers := map[string]string{ + "Content-Type": "application/yaml", + "X-Grafana-Alerting-Config-Identifier": identifier, + "X-Grafana-Alerting-Merge-Matchers": mergeMatchers, + } + var amConfig apimodels.AlertmanagerUserConfig + require.NoError(t, yaml.Unmarshal(configYaml, &amConfig)) + + response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers) + require.Equal(t, "success", response.Status) + + templates, err := client.List(context.Background(), metav1.ListOptions{}) + + require.NoError(t, err) + require.Len(t, templates.Items, 3) + + require.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title) + require.Equal(t, "imported", templates.Items[1].Spec.Title) + require.Equal(t, "template", templates.Items[2].Spec.Title) + + t.Run("should be correct kind", func(t *testing.T) { + assert.Equal(t, + v0alpha1.TemplateGroupSpec{ + Title: "imported", + Content: amConfig.TemplateFiles["imported"], + Kind: v0alpha1.TemplateGroupTemplateKindMimir, + }, templates.Items[1].Spec) + assert.Equal(t, + v0alpha1.TemplateGroupSpec{ + Title: "template", + Content: amConfig.TemplateFiles["template"], + Kind: v0alpha1.TemplateGroupTemplateKindMimir, + }, templates.Items[2].Spec) + }) + + t.Run("should be provisioned", func(t *testing.T) { + for _, tpl := range templates.Items[1:] { + assert.EqualValues(t, models.ProvenanceConvertedPrometheus, tpl.GetProvenanceStatus()) + } + }) + + t.Run("should not be able to update", func(t *testing.T) { + tpl := templates.Items[1] + tpl.Spec.Content = "new content" + _, err := client.Update(context.Background(), &tpl, metav1.UpdateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) + + t.Run("should not be able to delete", func(t *testing.T) { + err := client.Delete(context.Background(), templates.Items[1].Name, metav1.DeleteOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) + + t.Run("should not conflict with Grafana kind", func(t *testing.T) { + tpl := v0alpha1.TemplateGroup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: templates.Items[1].Spec, + } + tpl.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana + + created, err := client.Create(context.Background(), &tpl, metav1.CreateOptions{}) + require.NoError(t, err) + + assert.NotEqual(t, templates.Items[1].Name, created.Name) + }) + + t.Run("sort by kind and then name", func(t *testing.T) { + templates, err := client.List(context.Background(), metav1.ListOptions{}) + + require.NoError(t, err) + require.Len(t, templates.Items, 4) + assert.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title) + assert.Equal(t, "imported", templates.Items[1].Spec.Title) + assert.Equal(t, v0alpha1.TemplateGroupTemplateKindGrafana, templates.Items[1].Spec.Kind) + assert.Equal(t, "imported", templates.Items[2].Spec.Title) + assert.Equal(t, v0alpha1.TemplateGroupTemplateKindMimir, templates.Items[2].Spec.Kind) + assert.Equal(t, "template", templates.Items[3].Spec.Title) + }) +} diff --git a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go index 04d9e626ab7..146bccd5f11 100644 --- a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go +++ b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go @@ -54,6 +54,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "templateGroup", Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } @@ -112,6 +113,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) { require.Equal(t, v0alpha1.TemplateGroupSpec{ Title: v0alpha1.DefaultTemplateTitle, Content: defaultDefn.Template, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, actual.Spec) defaultTemplateGroup = actual }) @@ -226,6 +228,7 @@ func TestIntegrationAccessControl(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: fmt.Sprintf("template-group-1-%s", tc.user.Identity.GetLogin()), Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } expected.SetProvenanceStatus("") @@ -385,6 +388,7 @@ func TestIntegrationProvisioning(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "template-group-1", Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, }, v1.CreateOptions{}) require.NoError(t, err) @@ -428,6 +432,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "template-group-1", Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } @@ -510,6 +515,7 @@ func TestIntegrationPatch(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "template-group", Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } @@ -568,6 +574,7 @@ func TestIntegrationListSelector(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "test1", Content: `{{ define "test1" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } template1, err := adminClient.Create(ctx, template1, v1.CreateOptions{}) @@ -580,6 +587,7 @@ func TestIntegrationListSelector(t *testing.T) { Spec: v0alpha1.TemplateGroupSpec{ Title: "test2", Content: `{{ define "test2" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindGrafana, }, } template2, err = adminClient.Create(ctx, template2, v1.CreateOptions{}) @@ -655,3 +663,37 @@ func TestIntegrationListSelector(t *testing.T) { require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name) }) } + +func TestIntegrationKinds(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + helper := getTestHelper(t) + client := common.NewTemplateGroupClient(t, helper.Org1.Admin) + + newTemplate := &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "templateGroup", + Content: `{{ define "test" }} test {{ end }}`, + Kind: v0alpha1.TemplateGroupTemplateKindMimir, + }, + } + + t.Run("should not let create Mimir template", func(t *testing.T) { + _, err := client.Create(ctx, newTemplate, v1.CreateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) + + t.Run("should not let change kind", func(t *testing.T) { + newTemplate.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana + created, err := client.Create(ctx, newTemplate, v1.CreateOptions{}) + require.NoError(t, err) + + created.Spec.Kind = v0alpha1.TemplateGroupTemplateKindMimir + _, err = client.Update(ctx, created, v1.UpdateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) +} diff --git a/pkg/tests/apis/alerting/notifications/templategroup/test-data/imported.yaml b/pkg/tests/apis/alerting/notifications/templategroup/test-data/imported.yaml new file mode 100644 index 00000000000..76e9a2c4b51 --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/templategroup/test-data/imported.yaml @@ -0,0 +1,15 @@ +alertmanager_config: | + route: + receiver: sinkhole + group_by: + - alertname + receivers: + - name: sinkhole +template_files: + imported: | + {{ define "imported" }} + {{ end }} + template: | + {{ define "template" }} + {{ template "imported" . }} + {{ end }} diff --git a/pkg/tests/apis/collections/stars_test.go b/pkg/tests/apis/collections/stars_test.go index fe841fb4851..949e3d6c6ac 100644 --- a/pkg/tests/apis/collections/stars_test.go +++ b/pkg/tests/apis/collections/stars_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "testing" "github.com/stretchr/testify/require" @@ -41,9 +42,10 @@ func TestIntegrationStars(t *testing.T) { } helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs - DisableAnonymous: true, - EnableFeatureToggles: flags, + DisableDataMigrations: true, + AppModeProduction: false, // required for experimental APIs + DisableAnonymous: true, + EnableFeatureToggles: flags, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { DualWriterMode: mode, @@ -160,7 +162,7 @@ func TestIntegrationStars(t *testing.T) { { "group": "dashboard.grafana.app", "kind": "Dashboard", - "names": []string{"test-2", "aaa", "bbb"}, + "names": []string{"test-2", "aaa", "aaa", "bbb"}, }, }, }, @@ -174,13 +176,17 @@ func TestIntegrationStars(t *testing.T) { require.Equal(t, "dashboard.grafana.app", resources[0].Group) require.Equal(t, "Dashboard", resources[0].Kind) require.ElementsMatch(t, - []string{"aaa", "bbb", "test-2"}, // NOTE 2 stays, 3 removed, added aaa+bbb (and sorted!) + []string{"test-2", "aaa", "bbb"}, // keeps the requested order, removing duplicates resources[0].Names) rspObj, err = starsClient.Resource.Get(ctx, "user-"+starsClient.Args.User.Identity.GetIdentifier(), metav1.GetOptions{}) require.NoError(t, err) after = typed(t, rspObj, &collections.Stars{}) + + // FIXME: when we remove legacy support this should not sort! + slices.Sort(after.Spec.Resource[0].Names) + jj, err := json.MarshalIndent(after.Spec, "", " ") require.NoError(t, err) require.JSONEq(t, `{ diff --git a/pkg/tests/apis/config_test.go b/pkg/tests/apis/config_test.go index bf00affdfff..665d79a5e50 100644 --- a/pkg/tests/apis/config_test.go +++ b/pkg/tests/apis/config_test.go @@ -15,19 +15,19 @@ const pluginsDiscoveryJSON = `[ "freshness": "Current", "resources": [ { - "resource": "pluginmetas", + "resource": "metas", "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "scope": "Namespaced", - "singularResource": "pluginmeta", + "singularResource": "meta", "subresources": [ { "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "subresource": "status", @@ -51,7 +51,7 @@ const pluginsDiscoveryJSON = `[ "version": "" }, "scope": "Namespaced", - "singularResource": "plugins", + "singularResource": "plugin", "subresources": [ { "responseKind": { diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 85f369c2ac6..5e34d28ba9f 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -37,7 +37,8 @@ func TestMain(m *testing.M) { 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, + DisableAnonymous: true, + DisableDataMigrations: true, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { DualWriterMode: mode, @@ -194,7 +195,9 @@ func TestIntegrationLegacySupport(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) ctx := context.Background() - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{}) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableDataMigrations: true, + }) clientV0 := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, @@ -293,6 +296,199 @@ func TestIntegrationLegacySupport(t *testing.T) { require.Equal(t, dashboardV0.VERSION, rsp.Result.Meta.APIVersion) } +func TestIntegrationListPagination(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + gvr := schema.GroupVersionResource{ + Group: dashboardV0.GROUP, + Version: dashboardV0.VERSION, + Resource: "dashboards", + } + + // Test on modes with legacy + modes := []rest.DualWriterMode{rest.Mode1, rest.Mode2, rest.Mode3} + for _, mode := range modes { + t.Run(fmt.Sprintf("pagination with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + DisableDataMigrations: 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, + GVR: gvr, + }) + + // Test 1: List with no dashboards + rsp, err := client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, rsp.Items, 0) + + // Create 5 dashboards to test pagination with small limits + const totalDashboards = 5 + createdNames := make([]string, 0, totalDashboards) + for i := 0; i < totalDashboards; i++ { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]any{ + "title": fmt.Sprintf("Pagination test dashboard %d", i), + "schemaVersion": 42, + }, + }, + } + obj.SetGenerateName("pag-") + obj.SetAPIVersion(gvr.GroupVersion().String()) + obj.SetKind("Dashboard") + created, err := client.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + createdNames = append(createdNames, created.GetName()) + } + + // Test 2: List all without limit - should return all dashboards + rsp, err = client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, rsp.Items, totalDashboards, "should return all %d dashboards", totalDashboards) + + // Test 3: List with small limit (2) - should paginate + const pageSize = 2 + allNames := make(map[string]bool) + continueToken := "" + pageCount := 0 + + for { + pageCount++ + listOpts := metav1.ListOptions{ + Limit: pageSize, + Continue: continueToken, + } + rsp, err = client.Resource.List(ctx, listOpts) + require.NoError(t, err) + + // Collect names from this page + for _, item := range rsp.Items { + name := item.GetName() + require.False(t, allNames[name], "duplicate item %s found across pages", name) + allNames[name] = true + } + + // Check if there's more pages + continueToken = rsp.GetContinue() + if continueToken == "" { + break + } + + // Safety check to prevent infinite loops + require.Less(t, pageCount, 5) + } + + // Verify we got all dashboards across all pages + require.Len(t, allNames, totalDashboards, "should have collected all %d dashboards across pages", totalDashboards) + + // Verify all created dashboards were found + for _, name := range createdNames { + require.True(t, allNames[name], "dashboard %s not found in paginated results", name) + } + }) + + t.Run(fmt.Sprintf("history pagination with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + DisableDataMigrations: 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, + GVR: gvr, + }) + + // Create a dashboard + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]any{ + "title": "History pagination test dashboard", + "schemaVersion": 42, + }, + }, + } + obj.SetGenerateName("hist-") + obj.SetAPIVersion(gvr.GroupVersion().String()) + obj.SetKind("Dashboard") + created, err := client.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + dashName := created.GetName() + + // Update the dashboard multiple times to create history entries + const totalVersions = 5 + for i := 1; i < totalVersions; i++ { + // Get latest version + current, err := client.Resource.Get(ctx, dashName, metav1.GetOptions{}) + require.NoError(t, err) + + // Update title + spec := current.Object["spec"].(map[string]interface{}) + spec["title"] = fmt.Sprintf("History pagination test dashboard v%d", i+1) + current.Object["spec"] = spec + + _, err = client.Resource.Update(ctx, current, metav1.UpdateOptions{}) + require.NoError(t, err) + } + + // Test: List history with pagination + labelSelector := utils.LabelKeyGetHistory + "=true" + fieldSelector := "metadata.name=" + dashName + + const pageSize int64 = 2 + allVersions := make([]string, 0) + continueToken := "" + pageCount := 0 + + for { + pageCount++ + listOpts := metav1.ListOptions{ + LabelSelector: labelSelector, + FieldSelector: fieldSelector, + Limit: pageSize, + Continue: continueToken, + } + rsp, err := client.Resource.List(ctx, listOpts) + require.NoError(t, err) + + // Collect resource versions from this page + for _, item := range rsp.Items { + rv := item.GetResourceVersion() + allVersions = append(allVersions, rv) + } + + // Check if there's more pages + continueToken = rsp.GetContinue() + if continueToken == "" { + break + } + + // Safety check to prevent infinite loops + require.Less(t, pageCount, 5) + } + + // Verify we got all history versions + require.Len(t, allVersions, totalVersions, "should have collected all %d history versions across pages", totalVersions) + }) + } +} + func TestIntegrationSearchTypeFiltering(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -307,9 +503,10 @@ func runDashboardSearchTest(t *testing.T, mode rest.DualWriterMode) { ctx := context.Background() helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + AppModeProduction: true, + DisableDataMigrations: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": {DualWriterMode: mode}, "folders.folder.grafana.app": {DualWriterMode: mode}, diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 9e087cb76a6..ee40bf00b47 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -68,7 +68,8 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { // Create a K8sTestHelper which will set up a real API server helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesDashboards, // Enable FE-only dashboard feature flag }, @@ -105,7 +106,8 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) { t.Run(fmt.Sprintf("DualWriterMode %d - kubernetesDashboards disabled", dualWriterMode), func(t *testing.T) { // Create a K8sTestHelper which will set up a real API server helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, DisableFeatureToggles: []string{ featuremgmt.FlagKubernetesDashboards, }, @@ -130,52 +132,6 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) { } } -// TestIntegrationDashboardAPIAuthorization tests the dashboard K8s API with authorization checks -func TestIntegrationDashboardAPIAuthorization(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} - for _, dualWriterMode := range dualWriterModes { - t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: dualWriterMode, - }, - "folders.folder.grafana.app": { - DualWriterMode: dualWriterMode, - }, - }, - UnifiedStorageEnableSearch: true, - }) - - t.Cleanup(func() { - helper.Shutdown() - }) - - org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) - org2Ctx := createTestContext(t, helper, helper.OrgB, dualWriterMode) - - t.Run("Authorization tests for all identity types", func(t *testing.T) { - runAuthorizationTests(t, org1Ctx) - }) - - t.Run("Dashboard permission tests", func(t *testing.T) { - runDashboardPermissionTests(t, org1Ctx, true) - }) - - t.Run("Cross-organization tests", func(t *testing.T) { - runCrossOrgTests(t, org1Ctx, org2Ctx) - }) - - t.Run("Dashboard HTTP API test", func(t *testing.T) { - runDashboardHttpTest(t, org1Ctx, org2Ctx) - }) - }) - } -} - // TestIntegrationDashboardAPI tests the dashboard K8s API func TestIntegrationDashboardAPI(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -185,7 +141,8 @@ func TestIntegrationDashboardAPI(t *testing.T) { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { // Create a K8sTestHelper which will set up a real API server helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesDashboards, }, diff --git a/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go b/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go index dd85fdb2400..32548d593ea 100644 --- a/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/library_panels_api_validation_test.go @@ -29,7 +29,8 @@ func TestIntegrationLibraryPanelConnections(t *testing.T) { for _, dualWriterMode := range dualWriterModes { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, EnableFeatureToggles: []string{ "kubernetesLibraryPanels", }, @@ -93,7 +94,8 @@ func TestIntegrationLibraryElementPermissions(t *testing.T) { for _, dualWriterMode := range dualWriterModes { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, EnableFeatureToggles: []string{ "kubernetesLibraryPanels", "grafanaAPIServerWithExperimentalAPIs", // needed until we move it to v0beta1 at least (currently v0alpha1) @@ -296,7 +298,8 @@ func TestIntegrationLibraryPanelConnectionsWithFolderAccess(t *testing.T) { for _, dualWriterMode := range dualWriterModes { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, + DisableDataMigrations: true, + DisableAnonymous: true, EnableFeatureToggles: []string{ "kubernetesLibraryPanels", }, diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 09d23398c3f..3fe35c51b3a 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -37,9 +37,10 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { ctx := context.Background() helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": {DualWriterMode: mode}, "folders.folder.grafana.app": {DualWriterMode: mode}, diff --git a/pkg/tests/apis/folder/folder_tree_test.go b/pkg/tests/apis/folder/folder_tree_test.go index b1d38ab3918..613d021b236 100644 --- a/pkg/tests/apis/folder/folder_tree_test.go +++ b/pkg/tests/apis/folder/folder_tree_test.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/stretchr/testify/require" "github.com/xlab/treeprint" @@ -31,6 +32,33 @@ import ( "github.com/grafana/grafana/pkg/util/testutil" ) +func TestIntegrationFolderTreeZanzana(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + runIntegrationFolderTree(t, testinfra.GrafanaOpts{ + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: grafanarest.Mode5, + }, + folderV1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode5, + }, + }, + EnableFeatureToggles: []string{ + "zanzana", + "zanzanaNoLegacyClient", + "kubernetesAuthzZanzanaSync", + }, + UnifiedStorageEnableSearch: true, + ZanzanaReconciliationInterval: 100 * time.Millisecond, + DisableZanzanaCache: true, + }) +} + func TestIntegrationFolderTree(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -47,10 +75,11 @@ func TestIntegrationFolderTree(t *testing.T) { } for _, mode := range modes { t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + runIntegrationFolderTree(t, testinfra.GrafanaOpts{ + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { DualWriterMode: mode, @@ -61,113 +90,122 @@ func TestIntegrationFolderTree(t *testing.T) { }, UnifiedStorageEnableSearch: mode >= grafanarest.Mode3, // make sure modes 0-3 work without search enabled }) - defer helper.Shutdown() + }) + } +} - tests := []struct { - Name string - Definition FolderDefinition - Expected []ExpectedTree - }{ - { - Name: "admin-only-tree", - Definition: FolderDefinition{ +func runIntegrationFolderTree(t *testing.T, opts testinfra.GrafanaOpts) { + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + + helper := apis.NewK8sTestHelper(t, opts) + defer helper.Shutdown() + + apis.AwaitZanzanaReconcileNext(t, helper) + + tests := []struct { + Name string + Definition FolderDefinition + Expected []ExpectedTree + }{ + { + Name: "admin-only-tree", + Definition: FolderDefinition{ + Children: []FolderDefinition{ + {Name: "top", + Creator: helper.Org1.Admin, Children: []FolderDefinition{ - {Name: "top", + {Name: "middle", Creator: helper.Org1.Admin, Children: []FolderDefinition{ - {Name: "middle", + {Name: "child", Creator: helper.Org1.Admin, - Children: []FolderDefinition{ - {Name: "child", - Creator: helper.Org1.Admin, - Permissions: []FolderPermission{{ - Permission: "View", - User: helper.Org1.None, - }}, - }, - }, + Permissions: []FolderPermission{{ + Permission: "View", + User: helper.Org1.None, + }}, }, }, }, }, }, - Expected: []ExpectedTree{ - {User: helper.Org1.Admin, Listing: ` + }, + }, + Expected: []ExpectedTree{ + {User: helper.Org1.Admin, Listing: ` └── top (admin,edit,save,delete) ....└── middle (admin,edit,save,delete) ........└── child (admin,edit,save,delete)`}, - {User: helper.Org1.Viewer, Listing: ` + {User: helper.Org1.Viewer, Listing: ` └── top (view) ....└── middle (view) ........└── child (view)`}, - {User: helper.Org1.None, Listing: ` + {User: helper.Org1.None, Listing: ` └── sharedwithme (???) ....└── child (view)`, - E403: []string{"top", "middle"}, - }, - }, + E403: []string{"top", "middle"}, }, - } + }, + }, + } - var statusCode int - for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { - tt.Definition.RequireUniqueName(t, make(map[string]bool)) + var statusCode int + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + tt.Definition.RequireUniqueName(t, make(map[string]bool)) - tt.Definition.CreateWithLegacyAPI(t, helper, "") - // CreateWithLegacyAPI + tt.Definition.CreateWithLegacyAPI(t, helper, "") - for _, expect := range tt.Expected { - unstructured, client := getFolderClients(t, expect.User) - t.Run(fmt.Sprintf("query as %s", expect.User.Identity.GetLogin()), func(t *testing.T) { - legacy := getFoldersFromLegacyAPISearch(t, client) - legacy.requireEqual(t, expect.Listing, "legacy") + for _, expect := range tt.Expected { + unstructured, client := getFolderClients(t, expect.User) + t.Run(fmt.Sprintf("query as %s", expect.User.Identity.GetLogin()), func(t *testing.T) { + legacy := getFoldersFromLegacyAPISearch(t, client) + legacy.requireEqual(t, expect.Listing, "legacy") - listed := getFoldersFromAPIServerList(t, unstructured) - listed.requireEqual(t, expect.Listing, "listed") + listed := getFoldersFromAPIServerList(t, unstructured) + listed.requireEqual(t, expect.Listing, "listed") - search := getFoldersFromDashboardV0Search(t, client, expect.User.Identity.GetNamespace()) - search.requireEqual(t, expect.Listing, "search") + search := getFoldersFromDashboardV0Search(t, client, expect.User.Identity.GetNamespace()) + search.requireEqual(t, expect.Listing, "search") - // ensure sure GET also works on each folder we can list - listed.forEach(func(fv *FolderView) { - if fv.Name == folder.SharedWithMeFolderUID { - return // skip it - } - found, err := unstructured.Get(context.Background(), fv.Name, v1.GetOptions{}) - require.NoErrorf(t, err, "getting folder: %s", fv.Name) - require.Equal(t, found.GetName(), fv.Name) - }) + // ensure sure GET also works on each folder we can list + listed.forEach(func(fv *FolderView) { + if fv.Name == folder.SharedWithMeFolderUID { + return // skip it + } + found, err := unstructured.Get(context.Background(), fv.Name, v1.GetOptions{}) + require.NoErrorf(t, err, "getting folder: %s", fv.Name) + require.Equal(t, found.GetName(), fv.Name) + }) - // Forbidden things should really be hidden - for _, name := range expect.E403 { - _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}) - require.Error(t, err) - require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + // Forbidden things should really be hidden + for _, name := range expect.E403 { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}) + require.Error(t, err) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? - result := client.Get().AbsPath("api", "folders", name). - Do(context.Background()). - StatusCode(&statusCode) - require.Equal(t, int(http.StatusForbidden), statusCode) - require.Error(t, result.Error()) + result := client.Get().AbsPath("api", "folders", name). + Do(context.Background()). + StatusCode(&statusCode) + require.Equal(t, int(http.StatusForbidden), statusCode) + require.Error(t, result.Error()) - // Verify sub-resources are hidden - for _, sub := range []string{"access", "parents", "children", "counts"} { - _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}, sub) - require.Error(t, err, "expect error for subresource", sub) - require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? - } + // Verify sub-resources are hidden + for _, sub := range []string{"access", "parents", "children", "counts"} { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}, sub) + require.Error(t, err, "expect error for subresource", sub) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + } - // Verify legacy API access is also hidden - for _, sub := range []string{"permissions", "counts"} { - result := client.Get().AbsPath("api", "folders", name, sub). - Do(context.Background()). - StatusCode(&statusCode) - require.Equalf(t, int(http.StatusForbidden), statusCode, "legacy access to: %s", sub) - require.Error(t, result.Error()) - } - } - }) + // Verify legacy API access is also hidden + for _, sub := range []string{"permissions", "counts"} { + result := client.Get().AbsPath("api", "folders", name, sub). + Do(context.Background()). + StatusCode(&statusCode) + require.Equalf(t, int(http.StatusForbidden), statusCode, "legacy access to: %s", sub) + require.Error(t, result.Error()) + } } }) } @@ -211,6 +249,8 @@ func (f *FolderDefinition) CreateWithLegacyAPI(t *testing.T, h *apis.K8sTestHelp }) require.NoError(t, err) + apis.AwaitZanzanaReconcileNext(t, h) + var statusCode int result := client.Post().AbsPath("api", "folders"). Body(body). diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 43db6ff0932..ce7dd5f6795 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -139,9 +139,10 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v)", modeDw), func(t *testing.T) { doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -153,9 +154,10 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create nested folders)", modeDw), func(t *testing.T) { doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -166,9 +168,10 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create existing folder)", modeDw), func(t *testing.T) { doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -179,9 +182,10 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run(fmt.Sprintf("when creating a folder, mode %v, it should trim leading and trailing spaces", modeDw), func(t *testing.T) { doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -192,9 +196,10 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create circular reference folder)", modeDw), func(t *testing.T) { doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -218,9 +223,10 @@ func TestIntegrationFoldersApp(t *testing.T) { for _, mode := range modes { t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) { doListFoldersTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: mode, @@ -735,9 +741,10 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { 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", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -862,9 +869,10 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { 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", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1079,9 +1087,10 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { for mode := 0; mode <= 4; mode++ { modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1254,9 +1263,10 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1366,9 +1376,10 @@ func TestIntegrationFolderDeletionBlockedByLibraryElements(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1446,9 +1457,10 @@ func TestIntegrationRootFolderDeletionBlockedByLibraryElementsInSubfolder(t *tes modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1542,9 +1554,10 @@ func TestIntegrationFolderDeletionBlockedByConnectedLibraryPanels(t *testing.T) modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1620,9 +1633,10 @@ func TestIntegrationFolderDeletionWithDanglingLibraryPanels(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -1879,6 +1893,14 @@ func TestIntegrationMoveNestedFolderToRootK8S(t *testing.T) { require.Equal(t, http.StatusOK, get.Response.StatusCode) require.Equal(t, "f2", get.Result.UID) require.Equal(t, "", get.Result.ParentUID) + + // Check that we can get the same folder using metadata.name selector + results, err := client.Resource.List(context.Background(), metav1.ListOptions{ + FieldSelector: "metadata.name=" + get.Result.UID, + }) + require.NoError(t, err) + require.Len(t, results.Items, 1) + require.Equal(t, "f2", results.Items[0].GetName()) } // Test deleting nested folders ensures postorder deletion @@ -1893,9 +1915,10 @@ func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) { 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", + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -2019,9 +2042,10 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) { t.Run(fmt.Sprintf("Mode %d: Delete provisioned folders and dashboards", mode), func(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) ops := testinfra.GrafanaOpts{ - DisableAnonymous: true, - AppModeProduction: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + DisableAnonymous: true, + AppModeProduction: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: modeDw, @@ -2123,11 +2147,14 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) { // Test that folders created during provisioning using the dual writer have the // appropriate labels and annotations in unified storage. func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + mode3 := grafanarest.DualWriterMode(3) ops := testinfra.GrafanaOpts{ - DisableAnonymous: true, - AppModeProduction: true, - APIServerStorageType: "unified", + DisableDataMigrations: true, + DisableAnonymous: true, + AppModeProduction: true, + APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ folders.RESOURCEGROUP: { DualWriterMode: mode3, diff --git a/pkg/tests/apis/iam/resource_permissions_integration_test.go b/pkg/tests/apis/iam/resource_permissions_integration_test.go new file mode 100644 index 00000000000..dbfa51750ad --- /dev/null +++ b/pkg/tests/apis/iam/resource_permissions_integration_test.go @@ -0,0 +1,625 @@ +package identity + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "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" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +var gvrResourcePermissions = schema.GroupVersionResource{ + Group: "iam.grafana.app", + Version: "v0alpha1", + Resource: "resourcepermissions", +} + +var gvrFolders = schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v1beta1", + Resource: "folders", +} + +var gvrDashboards = schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v1beta1", + Resource: "dashboards", +} + +type permission struct { + kind string + name string + verb string +} + +func newPermission(kind, name, verb string) permission { + return permission{ + kind: kind, + name: name, + verb: verb, + } +} + +func (p permission) ToMap() map[string]interface{} { + return map[string]interface{}{ + "kind": p.kind, + "name": p.name, + "verb": p.verb, + } +} + +func newPermissionMaps(permissions ...permission) []map[string]interface{} { + permissionsMaps := make([]map[string]interface{}, len(permissions)) + for i, permission := range permissions { + permissionsMaps[i] = permission.ToMap() + } + return permissionsMaps +} + +type k8sTestClients struct { + rpAdmin *apis.K8sResourceClient + rpEditor *apis.K8sResourceClient + rpViewer *apis.K8sResourceClient +} + +func newk8sTestHelperClients(helper *apis.K8sTestHelper) *k8sTestClients { + return &k8sTestClients{ + rpAdmin: helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.OrgID), + GVR: gvrResourcePermissions, + }), + rpEditor: helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Editor, + Namespace: helper.Namespacer(helper.Org1.OrgID), + GVR: gvrResourcePermissions, + }), + rpViewer: helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Viewer, + Namespace: helper.Namespacer(helper.Org1.OrgID), + GVR: gvrResourcePermissions, + }), + } +} + +func TestIntegrationResourcePermissions(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3} + for _, mode := range modes { + if mode >= rest.Mode3 { + t.Skip("Skipping ResourcePermission tests for Mode3+ because default permissions are not written through the new APIs") + continue + } + t.Run(fmt.Sprintf("ResourcePermission CRUD with dual writer mode %d", mode), func(t *testing.T) { + // Turn off authorization cache so permission changes apply right away in tests + t.Setenv("GF_AUTHORIZATION_CACHE_TTL", "0s") + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "resourcepermissions.iam.grafana.app": { + DualWriterMode: mode, + }, + "folders.folder.grafana.app": { + DualWriterMode: mode, + }, + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesAuthzResourcePermissionApis, + // Prevents nested folders from having default permissions + featuremgmt.FlagKubernetesDashboards, + }, + }) + + // Work around the default permissions applied on root folders + // so we can test the ResourcePermission APIs without the default permissions interfering + parentFolder := createRootFolderWithoutDefaultPermissions(t, helper) + parentUID := parentFolder.GetName() + + clients := newk8sTestHelperClients(helper) + doResourcePermissionCRUDTests(t, helper, clients, parentUID) + doResourcePermissionAuthzTests(t, helper, clients, parentUID) + doResourcePermissionHierarchyTests(t, helper, clients, parentUID) + doResourcePermissionListFilteringTests(t, helper, clients, parentUID) + // TODO: Add tests for External JWT authentication + // doResourcePermissionAccessPolicyTests(t, helper) + }) + } +} + +func doResourcePermissionCRUDTests(t *testing.T, helper *apis.K8sTestHelper, clients *k8sTestClients, parentUID string) { + t.Run("should create/get/update/delete ResourcePermission using the new APIs", func(t *testing.T) { + ctx := context.Background() + + // Create ResourcePermission for the folder + permission := newPermission("ServiceAccount", helper.Org1.ViewerServiceAccount.UID, "view") + toCreate := createResourcePermissionObject(parentUID, gvrFolders.Group, gvrFolders.Resource, permission) + + created, err := clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + + createdName := created.GetName() + require.NotEmpty(t, createdName) + + // Verify spec + spec := created.Object["spec"].(map[string]interface{}) + resource := spec["resource"].(map[string]interface{}) + require.Equal(t, gvrFolders.Group, resource["apiGroup"]) + require.Equal(t, gvrFolders.Resource, resource["resource"]) + require.Equal(t, parentUID, resource["name"]) + + // Get the ResourcePermission + fetched, err := clients.rpAdmin.Resource.Get(ctx, createdName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + require.Equal(t, createdName, fetched.GetName()) + + // Update the ResourcePermission + fetched.Object["spec"].(map[string]interface{})["permissions"] = newPermissionMaps( + newPermission("User", helper.Org1.Viewer.Identity.GetIdentifier(), "edit"), + ) + updated, err := clients.rpAdmin.Resource.Update(ctx, fetched, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, updated) + + updatedSpec := updated.Object["spec"].(map[string]interface{}) + permissions := updatedSpec["permissions"].([]interface{}) + require.Len(t, permissions, 1) + perm := permissions[0].(map[string]interface{}) + require.Equal(t, helper.Org1.Viewer.Identity.GetIdentifier(), perm["name"]) + require.Equal(t, "edit", perm["verb"]) + + // Delete should work + err = clients.rpAdmin.Resource.Delete(ctx, createdName, metav1.DeleteOptions{}) + require.NoError(t, err) + + _, err = clients.rpAdmin.Resource.Get(ctx, createdName, metav1.GetOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(404), statusErr.ErrStatus.Code) + }) + + t.Run("should return 404 for non-existent ResourcePermission", func(t *testing.T) { + ctx := context.Background() + + _, err := clients.rpAdmin.Resource.Get(ctx, "folder.grafana.app-folders-unknown", metav1.GetOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(404), statusErr.ErrStatus.Code) + }) +} + +func doResourcePermissionAuthzTests(t *testing.T, helper *apis.K8sTestHelper, clients *k8sTestClients, parentUID string) { + t.Run("admin can create/update/delete ResourcePermission", func(t *testing.T) { + ctx := context.Background() + + folder := createTestFolder(t, helper, helper.Org1.Admin, "test-folder-admin", parentUID) + folderUID := folder.GetName() + + permission := newPermission("User", helper.Org1.Admin.Identity.GetIdentifier(), "admin") + + toCreate := createResourcePermissionObject(folderUID, gvrFolders.Group, gvrFolders.Resource, permission) + created, err := clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + + createdName := created.GetName() + + // Get the created object + fetched, err := clients.rpAdmin.Resource.Get(ctx, createdName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + // Update should work + permission = newPermission("Team", helper.Org1.Staff.UID, "edit") + fetched.Object["spec"].(map[string]interface{})["permissions"] = []interface{}{permission.ToMap()} + + _, err = clients.rpAdmin.Resource.Update(ctx, fetched, metav1.UpdateOptions{}) + require.NoError(t, err) + + // Delete should work + err = clients.rpAdmin.Resource.Delete(ctx, createdName, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + + t.Run("editor cannot create ResourcePermission (insufficient permissions)", func(t *testing.T) { + ctx := context.Background() + + folder := createTestFolder(t, helper, helper.Org1.Admin, "test-folder-editor-deny", parentUID) + folderUID := folder.GetName() + + permission := newPermission("User", helper.Org1.Editor.Identity.GetIdentifier(), "admin") + toCreate := createResourcePermissionObject(folderUID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err := clients.rpEditor.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + + t.Run("viewer cannot create ResourcePermission (insufficient permissions)", func(t *testing.T) { + ctx := context.Background() + + folder := createTestFolder(t, helper, helper.Org1.Admin, "test-folder-viewer-deny", parentUID) + folderUID := folder.GetName() + + permission := newPermission("User", helper.Org1.Viewer.Identity.GetIdentifier(), "admin") + toCreate := createResourcePermissionObject(folderUID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err := clients.rpViewer.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + + t.Run("viewer can update ResourcePermission of folder they can admin", func(t *testing.T) { + ctx := context.Background() + + folder := createTestFolder(t, helper, helper.Org1.Admin, "test-folder-viewer-admin", parentUID) + folderUID := folder.GetName() + + // Grant admin permissions to the viewer + permission := newPermission("User", helper.Org1.Viewer.Identity.GetIdentifier(), "admin") + toCreate := createResourcePermissionObject(folderUID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err := clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + // As a Viewer we should now be able to get and update the ResourcePermission of the folder + fetched, err := clients.rpViewer.Resource.Get(ctx, "folder.grafana.app-folders-"+folderUID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + // Update the ResourcePermission to grant editor permissions + fetched.Object["spec"].(map[string]interface{})["permissions"] = newPermissionMaps( + newPermission("BasicRole", "Editor", "edit"), + newPermission("User", helper.Org1.Viewer.Identity.GetIdentifier(), "admin"), + ) + + _, err = clients.rpViewer.Resource.Update(ctx, fetched, metav1.UpdateOptions{}) + require.NoError(t, err) + }) +} + +func doResourcePermissionHierarchyTests(t *testing.T, helper *apis.K8sTestHelper, clients *k8sTestClients, parentUID string) { + permission := newPermission("BasicRole", "Editor", "admin") + + t.Run("should respect folder hierarchy for folder permissions", func(t *testing.T) { + ctx := context.Background() + + sub1 := createTestFolder(t, helper, helper.Org1.Admin, "sub1-folder-hierarchy", parentUID) + sub1UID := sub1.GetName() + + sub2 := createTestFolder(t, helper, helper.Org1.Admin, "sub2-folder-hierarchy", sub1UID) + sub2UID := sub2.GetName() + + toCreate := createResourcePermissionObject(sub2UID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err := clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Run("editor can update ResourcePermission of sub2", func(t *testing.T) { + fetched, err := clients.rpEditor.Resource.Get(ctx, "folder.grafana.app-folders-"+sub2UID, metav1.GetOptions{}) + require.NoError(t, err) + + fetched.Object["spec"].(map[string]interface{})["permissions"] = newPermissionMaps( + newPermission("BasicRole", "Editor", "admin"), + newPermission("User", helper.Org1.Viewer.Identity.GetIdentifier(), "edit"), + ) + _, err = clients.rpEditor.Resource.Update(ctx, fetched, metav1.UpdateOptions{}) + require.NoError(t, err) + }) + t.Run("editor cannot create ResourcePermission of sub1", func(t *testing.T) { + toCreate := createResourcePermissionObject(sub1UID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err := clients.rpEditor.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + + // Delete the ResourcePermission of sub2 + err = clients.rpAdmin.Resource.Delete(ctx, "folder.grafana.app-folders-"+sub2UID, metav1.DeleteOptions{}) + require.NoError(t, err) + + // Create a new ResourcePermission for sub2 + toCreate = createResourcePermissionObject(sub1UID, gvrFolders.Group, gvrFolders.Resource, permission) + _, err = clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Run("editor can create ResourcePermission of sub2 with parent folder permission", func(t *testing.T) { + toCreate := createResourcePermissionObject(sub2UID, gvrFolders.Group, gvrFolders.Resource, permission) + + _, err = clients.rpEditor.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + fetched, err := clients.rpEditor.Resource.Get(ctx, "folder.grafana.app-folders-"+sub2UID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + permissions := fetched.Object["spec"].(map[string]interface{})["permissions"] + require.Len(t, permissions, 1) + }) + }) + + t.Run("should respect folder hierarchy for dashboard permissions", func(t *testing.T) { + ctx := context.Background() + + // Create folder and a nested dashboard + folder := createTestFolder(t, helper, helper.Org1.Admin, "sub1-dashboard-hierarchy", parentUID) + folderUID := folder.GetName() + + dashboard := createTestDashboard(t, helper, helper.Org1.Admin, "sub2-dashboard", folderUID) + dashboardUID := dashboard.GetName() + + // Verify dashboard has parent folder annotation + annotations := dashboard.GetAnnotations() + require.Equal(t, folderUID, annotations[utils.AnnoKeyFolder]) + + t.Run("editor cannot create ResourcePermission of dashboard without parent folder permission", func(t *testing.T) { + toCreate := createResourcePermissionObject(dashboardUID, gvrDashboards.Group, gvrDashboards.Resource, permission) + _, err := clients.rpEditor.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + + // Admin creates ResourcePermission for parent folder + toCreate := createResourcePermissionObject(folderUID, gvrFolders.Group, gvrFolders.Resource, permission) + created, err := clients.rpAdmin.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + + t.Run("editor can create ResourcePermission of dashboard with parent folder permission", func(t *testing.T) { + toCreate := createResourcePermissionObject(dashboardUID, gvrDashboards.Group, gvrDashboards.Resource, permission) + _, err := clients.rpEditor.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + fetched, err := clients.rpEditor.Resource.Get(ctx, "dashboard.grafana.app-dashboards-"+dashboardUID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + permissions := fetched.Object["spec"].(map[string]interface{})["permissions"] + require.Len(t, permissions, 1) + }) + }) +} + +func doResourcePermissionListFilteringTests(t *testing.T, helper *apis.K8sTestHelper, clients *k8sTestClients, parentUID string) { + viewerCanAdmin := newPermission("BasicRole", "Viewer", "admin") + viewerCanView := newPermission("BasicRole", "Viewer", "view") + editorCanAdmin := newPermission("BasicRole", "Editor", "admin") + + ctx := context.Background() + + // Create two folders + editorFolder := createTestFolder(t, helper, helper.Org1.Admin, "editor-only-folder", parentUID) + editorFolderUID := editorFolder.GetName() + + viewerFolder := createTestFolder(t, helper, helper.Org1.Admin, "viewer-only-folder", parentUID) + viewerFolderUID := viewerFolder.GetName() + + dashboardViewerCanAdmin := createTestDashboard(t, helper, helper.Org1.Admin, "dashboard-in-editor-folder-viewer-can-admin", editorFolderUID) + dashboardViewerCanAdminUID := dashboardViewerCanAdmin.GetName() + dashboardViewerCanView := createTestDashboard(t, helper, helper.Org1.Admin, "dashboard-in-editor-folder-viewer-can-view", editorFolderUID) + dashboardViewerCanViewUID := dashboardViewerCanView.GetName() + + // Grant admin permissions to the viewer on folder2 and dashboard1 + rp1 := createResourcePermissionObject(viewerFolderUID, gvrFolders.Group, gvrFolders.Resource, viewerCanAdmin) + _, err := clients.rpAdmin.Resource.Create(ctx, rp1, metav1.CreateOptions{}) + require.NoError(t, err) + rp2 := createResourcePermissionObject(dashboardViewerCanAdminUID, gvrDashboards.Group, gvrDashboards.Resource, viewerCanAdmin) + _, err = clients.rpAdmin.Resource.Create(ctx, rp2, metav1.CreateOptions{}) + require.NoError(t, err) + // Grant admin permissions to the editor on folder1 + rp3 := createResourcePermissionObject(editorFolderUID, gvrFolders.Group, gvrFolders.Resource, editorCanAdmin) + _, err = clients.rpAdmin.Resource.Create(ctx, rp3, metav1.CreateOptions{}) + require.NoError(t, err) + // Grant view permissions to the viewer on dashboard2 + rp4 := createResourcePermissionObject(dashboardViewerCanViewUID, gvrDashboards.Group, gvrDashboards.Resource, viewerCanView) + _, err = clients.rpAdmin.Resource.Create(ctx, rp4, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Run("Admin can list all ResourcePermissions", func(t *testing.T) { + // Admin can list all ResourcePermissions + list, err := clients.rpAdmin.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, list) + + // Check that all expected items are present (there may be more from other tests) + itemNames := getNamesFromList(list) + require.Contains(t, itemNames, rp1.GetName(), "Admin should see viewer folder permission") + require.Contains(t, itemNames, rp2.GetName(), "Admin should see dashboard viewer can admin permission") + require.Contains(t, itemNames, rp3.GetName(), "Admin should see editor folder permission") + require.Contains(t, itemNames, rp4.GetName(), "Admin should see dashboard viewer can view permission") + }) + + t.Run("Viewer can list ResourcePermissions of folder2 and dashboard1", func(t *testing.T) { + list, err := clients.rpViewer.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, list) + + itemNames := getNamesFromList(list) + // Viewer should see permissions for resources they can admin + require.Contains(t, itemNames, rp1.GetName(), "Viewer should see viewer folder permission") + require.Contains(t, itemNames, rp2.GetName(), "Viewer should see dashboard viewer can admin permission") + + // Viewer should NOT see permissions for resources they cannot admin + require.NotContains(t, itemNames, rp3.GetName(), "Viewer should NOT see editor folder permission") + require.NotContains(t, itemNames, rp4.GetName(), "Viewer should NOT see dashboard viewer can view permission") + }) + t.Run("Editor can list ResourcePermissions of folder1 and its nested dashboards", func(t *testing.T) { + list, err := clients.rpEditor.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, list) + + itemNames := getNamesFromList(list) + // Editor has admin on editorFolder, so they should see: + // - rp3 (editorFolder permission) + // - rp2 (dashboard in editorFolder with admin permission) + // - rp4 (dashboard in editorFolder with view permission) + require.Contains(t, itemNames, rp2.GetName(), "Editor should see dashboard admin permission in their folder") + require.Contains(t, itemNames, rp3.GetName(), "Editor should see their folder permission") + require.Contains(t, itemNames, rp4.GetName(), "Editor should see dashboard view permission in their folder") + + // Editor should NOT see permissions for viewerFolder + require.NotContains(t, itemNames, rp1.GetName(), "Editor should NOT see viewer-only folder permission") + }) +} + +// Helper functions + +func createTestFolder(t *testing.T, helper *apis.K8sTestHelper, user apis.User, title string, parentUID string) *unstructured.Unstructured { + t.Helper() + ctx := context.Background() + + folderClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: user, + Namespace: helper.Namespacer(user.Identity.GetOrgID()), + GVR: gvrFolders, + }) + metadata := map[string]interface{}{ + "generateName": "test-folder-", + "namespace": helper.Namespacer(user.Identity.GetOrgID()), + } + + if parentUID != "" { + metadata["annotations"] = map[string]interface{}{ + utils.AnnoKeyFolder: parentUID, + } + } + + folder := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "folder.grafana.app/v1beta1", + "kind": "Folder", + "metadata": metadata, + "spec": map[string]interface{}{ + "title": title, + }, + }, + } + + created, err := folderClient.Resource.Create(ctx, folder, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + return created +} + +// Helper function to delete default permissions +func deleteDefaultPermissions(t *testing.T, client *apis.K8sResourceClient, resourceName string) { + ctx := context.Background() + // Delete the resource permission + err := client.Resource.Delete(ctx, resourceName, metav1.DeleteOptions{}) + require.NoError(t, err) + // Check if the resource permission is deleted + _, err = client.Resource.Get(ctx, resourceName, metav1.GetOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(404), statusErr.ErrStatus.Code) +} + +// Helper function to create a root folder without default permissions +func createRootFolderWithoutDefaultPermissions(t *testing.T, helper *apis.K8sTestHelper) *unstructured.Unstructured { + t.Helper() + + // Create folder as admin + folder := createTestFolder(t, helper, helper.Org1.Admin, "root-without-permissions", "") + folderUID := folder.GetName() + + // Delete default permissions + rpClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.OrgID), + GVR: gvrResourcePermissions, + }) + + // It would be better to create the folder without default permissions, but this is a workaround for the time being + deleteDefaultPermissions(t, rpClient, "folder.grafana.app-folders-"+folderUID) + + return folder +} + +func createTestDashboard(t *testing.T, helper *apis.K8sTestHelper, user apis.User, title, folderUID string) *unstructured.Unstructured { + t.Helper() + ctx := context.Background() + + dashboardClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: user, + Namespace: helper.Namespacer(user.Identity.GetOrgID()), + GVR: gvrDashboards, + }) + + dashboard := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v1beta1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "generateName": "test-dashboard-", + "namespace": helper.Namespacer(user.Identity.GetOrgID()), + "annotations": map[string]interface{}{ + utils.AnnoKeyFolder: folderUID, + }, + }, + "spec": map[string]interface{}{ + "title": title, + }, + }, + } + + created, err := dashboardClient.Resource.Create(ctx, dashboard, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + return created +} + +func createResourcePermissionObject(resourceName, apiGroup, resource string, permissions ...permission) *unstructured.Unstructured { + permissionMaps := newPermissionMaps(permissions...) + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": iamv0.GROUP + "/" + iamv0.VERSION, + "kind": "ResourcePermission", + "metadata": map[string]interface{}{ + "name": apiGroup + "-" + resource + "-" + resourceName, + }, + "spec": map[string]interface{}{ + "resource": map[string]interface{}{ + "apiGroup": apiGroup, + "resource": resource, + "name": resourceName, + }, + "permissions": permissionMaps, + }, + }, + } +} + +func getNamesFromList(list *unstructured.UnstructuredList) []string { + names := make([]string, len(list.Items)) + for i, item := range list.Items { + names[i] = item.GetName() + } + return names +} diff --git a/pkg/tests/apis/iam/team_search_integration_test.go b/pkg/tests/apis/iam/team_search_integration_test.go new file mode 100644 index 00000000000..bf02f5718c9 --- /dev/null +++ b/pkg/tests/apis/iam/team_search_integration_test.go @@ -0,0 +1,201 @@ +package identity + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationTeamSearch(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("Team search with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "teams.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + UnifiedStorageEnableSearch: true, + }) + doTeamSearchTests(t, helper) + }) + } +} + +func doTeamSearchTests(t *testing.T, helper *apis.K8sTestHelper) { + ctx := context.Background() + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + + // Create teams for testing + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: namespace, + GVR: gvrTeams, + }) + + team1, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml"), metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, team1) + + team2YAML := helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml") + team2YAML.Object["metadata"].(map[string]interface{})["name"] = "testteam2" + team2YAML.Object["spec"].(map[string]interface{})["title"] = "Another Team" + team2YAML.Object["spec"].(map[string]interface{})["email"] = "anotherteam@example.com" + + team2, err := teamClient.Resource.Create(ctx, team2YAML, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, team2) + + t.Run("should search teams without query parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams") + require.GreaterOrEqual(t, len(result.Hits), 2, "should return at least 2 hits") + + for _, hit := range result.Hits { + if hit.Name == team1.GetName() { + require.Equal(t, "Test Team 1", hit.Title) + require.Equal(t, "testteam1@example123.com", hit.Email) + } + if hit.Name == team2.GetName() { + require.Equal(t, "Another Team", hit.Title) + require.Equal(t, "anotherteam@example.com", hit.Email) + } + } + }) + + t.Run("should search teams with query parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=another", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, result.TotalHits, int64(1), "should find 1 team matching 'another'") + require.Equal(t, len(result.Hits), 1, "should return 1 hit") + require.Equal(t, result.Hits[0].Name, team2.GetName()) + require.Equal(t, result.Hits[0].Title, "Another Team") + require.Equal(t, result.Hits[0].Email, "anotherteam@example.com") + }) + + t.Run("should return no results when query does not match any teams", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=nonexistent", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, int64(0), result.TotalHits, "should return 0 hits when query does not match any teams") + require.Equal(t, 0, len(result.Hits), "should return 0 hits when query does not match any teams") + }) + + t.Run("should search teams with limit parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, 1, len(result.Hits), "should return 1 hit when limit is 1") + }) + + t.Run("should search teams with pagination", func(t *testing.T) { + // First page + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=1", namespace) + var result1 iamv0alpha1.TeamSearchResults + + response1 := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result1) + + require.NotNil(t, response1) + require.Equal(t, http.StatusOK, response1.Response.StatusCode) + require.NotNil(t, response1.Result) + require.Equal(t, int64(0), result1.Offset, "first page should have offset 0") + + // Second page + path2 := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=2", namespace) + var result2 iamv0alpha1.TeamSearchResults + + response2 := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path2, + }, &result2) + + require.NotNil(t, response2) + require.Equal(t, http.StatusOK, response2.Response.StatusCode) + require.NotNil(t, response2.Result) + require.Equal(t, int64(1), result2.Offset, "second page should have offset 1") + }) + + t.Run("should search teams with offset parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?offset=1&limit=1", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams") + require.Equal(t, 1, len(result.Hits), "should return 1 hit") + require.Equal(t, int64(1), result.Offset, "should return offset 1") + }) +} diff --git a/pkg/tests/apis/iam/user_search_integration_test.go b/pkg/tests/apis/iam/user_search_integration_test.go new file mode 100644 index 00000000000..1f689b701b0 --- /dev/null +++ b/pkg/tests/apis/iam/user_search_integration_test.go @@ -0,0 +1,568 @@ +package identity + +import ( + "context" + "fmt" + "net/url" + "sort" + "testing" + "time" + + "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" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationUserSearch(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("DualWriterMode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + UnifiedStorageEnableSearch: true, + }) + + t.Cleanup(func() { + helper.Shutdown() + }) + + setupUsers(t, helper) + + t.Run("search by title", func(t *testing.T) { + res := searchUsers(t, helper, "Alice") + require.Len(t, res.Hits, 1) + require.Equal(t, "TestUser Alice", res.Hits[0].Title) + }) + + t.Run("search by login", func(t *testing.T) { + res := searchUsers(t, helper, "bob") + require.Len(t, res.Hits, 1) + require.Equal(t, "bob", res.Hits[0].Login) + }) + + t.Run("search by email", func(t *testing.T) { + res := searchUsers(t, helper, "charlie@example.com") + require.Len(t, res.Hits, 1) + require.Equal(t, "charlie@example.com", res.Hits[0].Email) + }) + }) + } +} + +func TestIntegrationUserSearch_WithSorting(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("DualWriterMode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + UnifiedStorageEnableSearch: true, + }) + + t.Cleanup(func() { + helper.Shutdown() + }) + + setupUsers(t, helper) + + tests := []struct { + field string + extractor func(iamv0.UserHit) string + expected []string + }{ + { + field: "title", + extractor: func(h iamv0.UserHit) string { return h.Title }, + expected: []string{"TestUser Alice", "TestUser Bob", "TestUser Charlie", "TestUser Editor", "TestUser Viewer"}, + }, + { + field: "login", + extractor: func(h iamv0.UserHit) string { return h.Login }, + expected: []string{"alice", "bob", "charlie", "testuser-editor", "testuser-viewer"}, + }, + { + field: "email", + extractor: func(h iamv0.UserHit) string { return h.Email }, + expected: []string{"alice@example.com", "bob@example.com", "charlie@example.com", "testuser-editor@example.com", "testuser-viewer@example.com"}, + }, + } + + for _, tc := range tests { + t.Run("sort by "+tc.field, func(t *testing.T) { + // ASC + res := searchUsersWithSort(t, helper, "TestUser", tc.field) + require.GreaterOrEqual(t, len(res.Hits), 5) + verifyOrder(t, res.Hits, tc.expected, tc.extractor) + + // DESC + res = searchUsersWithSort(t, helper, "TestUser", "-"+tc.field) + require.GreaterOrEqual(t, len(res.Hits), 5) + + // Reverse expected + reversed := make([]string, len(tc.expected)) + copy(reversed, tc.expected) + sort.Sort(sort.Reverse(sort.StringSlice(reversed))) + verifyOrder(t, res.Hits, reversed, tc.extractor) + }) + } + + t.Run("sort by lastSeenAt", func(t *testing.T) { + if mode >= rest.Mode3 { + t.Skip("Skipping lastSeenAt sort test for Mode >= 3: API does not persist status.lastSeenAt") + } + // Populate lastSeenAt + // Alice: 30 minutes ago + // Bob: 1 minute ago + // Charlie: 2 hours ago + // Editor: 40 minutes ago + // Viewer: 1h 30 mins ago + now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + updateLastSeenAt(t, helper, "alice", now.Add(-30*time.Minute), mode) + updateLastSeenAt(t, helper, "bob", now.Add(-1*time.Minute), mode) + updateLastSeenAt(t, helper, "charlie", now.Add(-2*time.Hour), mode) + updateLastSeenAt(t, helper, "testuser-editor", now.Add(-40*time.Minute), mode) + updateLastSeenAt(t, helper, "testuser-viewer", now.Add(-90*time.Minute), mode) + + // lastSeenAt ASC means oldest date first to match legacy behavior + res := searchUsersWithSort(t, helper, "TestUser", "lastSeenAt") + require.GreaterOrEqual(t, len(res.Hits), 5) + verifyOrder(t, res.Hits, []string{"charlie", "testuser-viewer", "testuser-editor", "alice", "bob"}, func(h iamv0.UserHit) string { return h.Login }) + + res = searchUsersWithSort(t, helper, "TestUser", "-lastSeenAt") + require.GreaterOrEqual(t, len(res.Hits), 5) + verifyOrder(t, res.Hits, []string{"bob", "alice", "testuser-editor", "testuser-viewer", "charlie"}, func(h iamv0.UserHit) string { return h.Login }) + }) + }) + } +} + +func TestIntegrationUserSearch_SortCompareLegacy(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode2} + for _, mode := range modes { + t.Run(fmt.Sprintf("DualWriterMode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + UnifiedStorageEnableSearch: true, + }) + + t.Cleanup(func() { + helper.Shutdown() + }) + + setupUsers(t, helper) + + // Populate lastSeenAt for sorting comparison + now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + updateLastSeenAt(t, helper, "alice", now.Add(-30*time.Minute), mode) + updateLastSeenAt(t, helper, "bob", now.Add(-1*time.Minute), mode) + updateLastSeenAt(t, helper, "charlie", now.Add(-2*time.Hour), mode) + updateLastSeenAt(t, helper, "testuser-editor", now.Add(-40*time.Minute), mode) + updateLastSeenAt(t, helper, "testuser-viewer", now.Add(-90*time.Minute), mode) + + fields := []string{"login", "email", "name", "lastSeenAt"} + for _, field := range fields { + for _, order := range []string{"asc", "desc"} { + t.Run(fmt.Sprintf("compare %s %s", field, order), func(t *testing.T) { + // Legacy API uses "name" for Name/Title, "login" for Login, "email" for Email. + // "lastSeenAt" maps to "lastSeenAtAge" in legacy. + legacySort := field + if field == "lastSeenAt" { + legacySort = "lastSeenAtAge" + } + legacySort += "-" + order + + newSort := field + if order == "desc" { + newSort = "-" + field + } + + legacyRes := searchUsersLegacy(t, helper, "TestUser", legacySort) + newRes := searchUsersWithSort(t, helper, "TestUser", newSort) + + require.Equal(t, len(legacyRes), len(newRes.Hits)) + for i := range legacyRes { + require.Equal(t, legacyRes[i].Login, newRes.Hits[i].Login, "Mismatch at index %d for sort %s", i, newSort) + } + }) + } + } + }) + } +} + +func TestIntegrationUserSearch_Paging(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("DualWriterMode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "users.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + UnifiedStorageEnableSearch: true, + }) + + t.Cleanup(func() { + helper.Shutdown() + }) + + setupUsers(t, helper) + + t.Run("paging with page and limit", func(t *testing.T) { + // There are 5 users matching "TestUser" + query := "TestUser" + + // Page 1, Limit 2 + res1 := searchUsersWithPaging(t, helper, query, 1, 2) + require.Equal(t, int64(5), res1.TotalHits) + require.Len(t, res1.Hits, 2) + + // Page 2, Limit 2 + res2 := searchUsersWithPaging(t, helper, query, 2, 2) + require.Equal(t, int64(5), res2.TotalHits) + require.Len(t, res2.Hits, 2) + + // Page 3, Limit 2 + res3 := searchUsersWithPaging(t, helper, query, 3, 2) + require.Equal(t, int64(5), res3.TotalHits) + require.Len(t, res3.Hits, 1) + + seen := make(map[string]bool) + for _, h := range res1.Hits { + seen[h.Login] = true + } + for _, h := range res2.Hits { + require.False(t, seen[h.Login], "User %s seen in page 1 and 2", h.Login) + seen[h.Login] = true + } + for _, h := range res3.Hits { + require.False(t, seen[h.Login], "User %s seen in previous pages", h.Login) + seen[h.Login] = true + } + require.Len(t, seen, 5) + }) + + t.Run("paging with offset and limit", func(t *testing.T) { + // There are 5 users matching "TestUser" + query := "TestUser" + + // Offset 0, Limit 2 (equivalent to Page 1) + res1 := searchUsersWithOffset(t, helper, query, 0, 2) + require.Equal(t, int64(5), res1.TotalHits) + require.Len(t, res1.Hits, 2) + + // Offset 2, Limit 2 (equivalent to Page 2) + res2 := searchUsersWithOffset(t, helper, query, 2, 2) + require.Equal(t, int64(5), res2.TotalHits) + require.Len(t, res2.Hits, 2) + + // Offset 4, Limit 2 (equivalent to Page 3) + res3 := searchUsersWithOffset(t, helper, query, 4, 2) + require.Equal(t, int64(5), res3.TotalHits) + require.Len(t, res3.Hits, 1) + + // Verify uniqueness + seen := make(map[string]bool) + for _, h := range res1.Hits { + seen[h.Login] = true + } + for _, h := range res2.Hits { + require.False(t, seen[h.Login], "User %s seen in offset 0 and 2", h.Login) + seen[h.Login] = true + } + for _, h := range res3.Hits { + require.False(t, seen[h.Login], "User %s seen in previous offsets", h.Login) + seen[h.Login] = true + } + require.Len(t, seen, 5) + }) + }) + } +} + +func setupUsers(t *testing.T, helper *apis.K8sTestHelper) { + ctx := context.Background() + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrUsers, + }) + + users := []iamv0.User{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser-editor", + }, + Spec: iamv0.UserSpec{ + Title: "TestUser Editor", + Login: "testuser-editor", + Email: "testuser-editor@example.com", + Role: "Editor", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser-viewer", + }, + Spec: iamv0.UserSpec{ + Title: "TestUser Viewer", + Login: "testuser-viewer", + Email: "testuser-viewer@example.com", + Role: "Viewer", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "alice", + }, + Spec: iamv0.UserSpec{ + Title: "TestUser Alice", + Login: "alice", + Email: "alice@example.com", + Role: "Viewer", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bob", + }, + Spec: iamv0.UserSpec{ + Title: "TestUser Bob", + Login: "bob", + Email: "bob@example.com", + Role: "Viewer", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "charlie", + }, + Spec: iamv0.UserSpec{ + Title: "TestUser Charlie", + Login: "charlie", + Email: "charlie@example.com", + Role: "Viewer", + }, + }, + } + + for _, u := range users { + uMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&u) + require.NoError(t, err) + _, err = userClient.Resource.Create(ctx, &unstructured.Unstructured{Object: uMap}, metav1.CreateOptions{}) + require.NoError(t, err) + } + + // Wait for indexing + time.Sleep(2 * time.Second) +} + +func searchUsers(t *testing.T, helper *apis.K8sTestHelper, query string) *iamv0.GetSearchUsers { + return searchUsersWithSort(t, helper, query, "") +} + +func searchUsersWithSort(t *testing.T, helper *apis.K8sTestHelper, query string, sort string) *iamv0.GetSearchUsers { + q := url.Values{} + q.Set("query", query) + if sort != "" { + q.Set("sort", sort) + } + q.Set("limit", "100") + + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/default/searchUsers?%s", q.Encode()) + + res := &iamv0.GetSearchUsers{} + rsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, res) + + require.Equal(t, 200, rsp.Response.StatusCode) + return res +} + +func searchUsersWithPaging(t *testing.T, helper *apis.K8sTestHelper, query string, page, limit int) *iamv0.GetSearchUsers { + q := url.Values{} + q.Set("query", query) + q.Set("page", fmt.Sprintf("%d", page)) + q.Set("limit", fmt.Sprintf("%d", limit)) + // Sort by login to ensure deterministic paging + q.Set("sort", "login") + + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/default/searchUsers?%s", q.Encode()) + + res := &iamv0.GetSearchUsers{} + rsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, res) + + require.Equal(t, 200, rsp.Response.StatusCode) + return res +} + +func searchUsersWithOffset(t *testing.T, helper *apis.K8sTestHelper, query string, offset, limit int) *iamv0.GetSearchUsers { + q := url.Values{} + q.Set("query", query) + q.Set("offset", fmt.Sprintf("%d", offset)) + q.Set("limit", fmt.Sprintf("%d", limit)) + // Sort by login to ensure deterministic paging + q.Set("sort", "login") + + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/default/searchUsers?%s", q.Encode()) + + res := &iamv0.GetSearchUsers{} + rsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, res) + + require.Equal(t, 200, rsp.Response.StatusCode) + return res +} + +type LegacyUserSearchHit struct { + UserId int64 `json:"userId"` + Name string `json:"name"` + Login string `json:"login"` + Email string `json:"email"` +} + +func searchUsersLegacy(t *testing.T, helper *apis.K8sTestHelper, query string, sort string) []LegacyUserSearchHit { + q := url.Values{} + q.Set("query", query) + + if sort != "" { + q.Set("sort", sort) + } + q.Set("perpage", "100") + q.Set("page", "1") + + path := fmt.Sprintf("/api/org/users/search?%s", q.Encode()) + + var res struct { + OrgUsers []LegacyUserSearchHit `json:"orgUsers"` + } + + rsp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &res) + + require.Equal(t, 200, rsp.Response.StatusCode) + return res.OrgUsers +} + +// verifyOrder checks that the extracted values from hits are in the expected order. +// It filters hits to only include those with expected values, because search returns more results than just the test users. +// Like other users in the system that have been created by the test framework. +func verifyOrder(t *testing.T, hits []iamv0.UserHit, expectedValues []string, extractor func(iamv0.UserHit) string) { + // Filter hits to only include expected values + var actualValues []string + expectedSet := make(map[string]bool) + for _, v := range expectedValues { + expectedSet[v] = true + } + + for _, h := range hits { + val := extractor(h) + if expectedSet[val] { + actualValues = append(actualValues, val) + } + } + + require.Equal(t, expectedValues, actualValues) +} + +func updateLastSeenAt(t *testing.T, helper *apis.K8sTestHelper, login string, lastSeen time.Time, mode rest.DualWriterMode) { + if mode < rest.Mode3 { + err := helper.GetEnv().SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Table("user").Where("login = ?", login).Update(map[string]interface{}{ + "last_seen_at": lastSeen, + }) + return err + }) + require.NoError(t, err) + } + + // Use the new APIs to update the user resource status in Mode3+ + if mode >= rest.Mode3 { + ctx := context.Background() + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrUsers, + }) + + u, err := userClient.Resource.Get(ctx, login, metav1.GetOptions{}) + require.NoError(t, err) + + err = unstructured.SetNestedField(u.Object, lastSeen.Unix(), "status", "lastSeenAt") + require.NoError(t, err) + + _, err = userClient.Resource.Update(ctx, u, metav1.UpdateOptions{}) + require.NoError(t, err) + } +} 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 b65fa2ad0d7..4634143bd45 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1802,6 +1802,15 @@ } } }, + { + "name": "facetLimit", + "in": "query", + "description": "maximum number of terms to return per facet (default 50, max 1000)", + "schema": { + "type": "integer", + "format": "int64" + } + }, { "name": "tags", "in": "query", @@ -2160,6 +2169,43 @@ ] } }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/settings": { + "get": { + "tags": [ + "Snapshot" + ], + "description": "Get Snapshot sharing settings", + "operationId": "getSnapshotSettings", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {}, + "example": "{\"snapshotsEnabled\":true,\"externalSnapshotURL\":\"https://externalurl.com\",\"externalSnapshotName\":\"external\",\"externalEnabled\":true}" + } + } + } + }, + "x-grafana-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "SnapshotSharingOptions" + } + } + }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/{name}": { "get": { "tags": [ 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 2cad6213d04..b0d21a3a60c 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3020,6 +3020,9 @@ "type": "string", "default": "" }, + "regexApplyTo": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo" + }, "skipUrlSync": { "type": "boolean", "default": false @@ -3930,6 +3933,14 @@ "onTimeRangeChanged" ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo": { + "description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)", + "type": "string", + "enum": [ + "value", + "text" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort": { "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", "type": "string", diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json index 198ad3aea25..f4ecc6c4599 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -3047,6 +3047,9 @@ "type": "string", "default": "" }, + "regexApplyTo": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo" + }, "skipUrlSync": { "type": "boolean", "default": false @@ -3957,6 +3960,14 @@ "onTimeRangeChanged" ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo": { + "description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)", + "type": "string", + "enum": [ + "value", + "text" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort": { "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", "type": "string", 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 ea0e4039581..dab9f3cd8b1 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -1021,6 +1021,156 @@ } } }, + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/searchUsers": { + "get": { + "tags": [ + "Search" + ], + "description": "User search", + "operationId": "getSearchUsers", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "number of results to return", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 30 + }, + { + "name": "page", + "in": "query", + "description": "page number (starting from 1)", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 1 + }, + { + "name": "offset", + "in": "query", + "description": "number of results to skip", + "schema": { + "type": "integer", + "format": "int64" + }, + "example": 0 + }, + { + "name": "sort", + "in": "query", + "description": "sortable field", + "schema": { + "type": "string" + }, + "examples": { + "": { + "summary": "default sorting" + }, + "-email": { + "summary": "email descending", + "value": "-email" + }, + "-lastSeenAt": { + "summary": "last seen at descending", + "value": "-lastSeenAt" + }, + "-login": { + "summary": "login descending", + "value": "-login" + }, + "-title": { + "summary": "title descending", + "value": "-title" + }, + "email": { + "summary": "email ascending", + "value": "email" + }, + "lastSeenAt": { + "summary": "last seen at ascending", + "value": "lastSeenAt" + }, + "login": { + "summary": "login ascending", + "value": "login" + }, + "title": { + "summary": "title ascending", + "value": "title" + } + } + } + ], + "responses": { + "default": { + "description": "Default OK response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "offset", + "totalHits", + "hits", + "queryCost", + "maxScore" + ], + "properties": { + "hits": { + "type": "array", + "items": { + "default": {} + } + }, + "maxScore": { + "type": "number", + "format": "double", + "default": 0 + }, + "offset": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "queryCost": { + "type": "number", + "format": "double", + "default": 0 + }, + "totalHits": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + } + } + } + } + } + } + }, "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/serviceaccounts": { "get": { "tags": [ @@ -5614,7 +5764,8 @@ "type": "object", "required": [ "metadata", - "spec" + "spec", + "status" ], "properties": { "apiVersion": { @@ -5641,6 +5792,14 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserSpec" } ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus" + } + ] } }, "x-kubernetes-group-version-kind": [ @@ -5741,6 +5900,19 @@ } } }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus": { + "type": "object", + "required": [ + "lastSeenAt" + ], + "properties": { + "lastSeenAt": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": { "type": "object", "required": [ @@ -6566,6 +6738,44 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchUsers": { + "type": "object", + "required": [ + "offset", + "totalHits", + "hits", + "queryCost", + "maxScore" + ], + "properties": { + "hits": { + "type": "array", + "items": { + "default": {} + } + }, + "maxScore": { + "type": "number", + "format": "double", + "default": 0 + }, + "offset": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "queryCost": { + "type": "number", + "format": "double", + "default": 0 + }, + "totalHits": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": { "type": "object", "required": [ @@ -7760,7 +7970,8 @@ "type": "object", "required": [ "metadata", - "spec" + "spec", + "status" ], "properties": { "apiVersion": { @@ -7777,6 +7988,63 @@ "spec": { "description": "Spec is the spec of the User", "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserHit": { + "type": "object", + "required": [ + "name", + "title", + "login", + "email", + "role", + "lastSeenAt", + "lastSeenAtAge", + "provisioned", + "score" + ], + "properties": { + "email": { + "type": "string", + "default": "" + }, + "lastSeenAt": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "lastSeenAtAge": { + "type": "string", + "default": "" + }, + "login": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + }, + "provisioned": { + "type": "boolean", + "default": false + }, + "role": { + "type": "string", + "default": "" + }, + "score": { + "type": "number", + "format": "double", + "default": 0 + }, + "title": { + "type": "string", + "default": "" } } }, @@ -7854,51 +8122,15 @@ } }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "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": "object", - "additionalProperties": { - "default": {} - } - } - } - }, - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": { "type": "object", "required": [ - "lastEvaluation", - "state" + "lastSeenAt" ], "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" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" + "lastSeenAt": { + "type": "integer", + "format": "int64", + "default": 0 } } }, diff --git a/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json index e1ce97830be..017323b2a1e 100644 --- a/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json @@ -865,296 +865,6 @@ } ] }, - "/apis/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigationindexes/{name}/status": { - "get": { - "tags": [ - "InvestigationIndex" - ], - "description": "read status of the specified InvestigationIndex", - "operationId": "getInvestigationIndexStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "InvestigationIndex" - } - }, - "put": { - "tags": [ - "InvestigationIndex" - ], - "description": "replace status of the specified InvestigationIndex", - "operationId": "replaceInvestigationIndexStatus", - "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.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "InvestigationIndex" - } - }, - "patch": { - "tags": [ - "InvestigationIndex" - ], - "description": "partially update status of the specified InvestigationIndex", - "operationId": "updateInvestigationIndexStatus", - "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.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "InvestigationIndex" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the InvestigationIndex", - "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/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigations": { "get": { "tags": [ @@ -1984,296 +1694,6 @@ } } ] - }, - "/apis/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigations/{name}/status": { - "get": { - "tags": [ - "Investigation" - ], - "description": "read status of the specified Investigation", - "operationId": "getInvestigationStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "Investigation" - } - }, - "put": { - "tags": [ - "Investigation" - ], - "description": "replace status of the specified Investigation", - "operationId": "replaceInvestigationStatus", - "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.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "Investigation" - } - }, - "patch": { - "tags": [ - "Investigation" - ], - "description": "partially update status of the specified Investigation", - "operationId": "updateInvestigationStatus", - "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.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "investigations.grafana.app", - "version": "v0alpha1", - "kind": "Investigation" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the Investigation", - "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": { diff --git a/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json index 25d6cd87807..93be607fd41 100644 --- a/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json @@ -35,6 +35,1126 @@ } } }, + "/apis/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "list or watch objects of kind LogsDrilldownDefaultColumns", + "operationId": "listLogsDrilldownDefaultColumns", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "post": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "create LogsDrilldownDefaultColumns", + "operationId": "createLogsDrilldownDefaultColumns", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "delete": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "delete collection of LogsDrilldownDefaultColumns", + "operationId": "deletecollectionLogsDrilldownDefaultColumns", + "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": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "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/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns/{name}": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "read the specified LogsDrilldownDefaultColumns", + "operationId": "getLogsDrilldownDefaultColumns", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "put": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "replace the specified LogsDrilldownDefaultColumns", + "operationId": "replaceLogsDrilldownDefaultColumns", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "delete": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "delete LogsDrilldownDefaultColumns", + "operationId": "deleteLogsDrilldownDefaultColumns", + "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": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "patch": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "partially update the specified LogsDrilldownDefaultColumns", + "operationId": "updateLogsDrilldownDefaultColumns", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LogsDrilldownDefaultColumns", + "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/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns/{name}/status": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "read status of the specified LogsDrilldownDefaultColumns", + "operationId": "getLogsDrilldownDefaultColumnsStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "put": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "replace status of the specified LogsDrilldownDefaultColumns", + "operationId": "replaceLogsDrilldownDefaultColumnsStatus", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "patch": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "partially update status of the specified LogsDrilldownDefaultColumns", + "operationId": "updateLogsDrilldownDefaultColumnsStatus", + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LogsDrilldownDefaultColumns", + "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/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaults": { "get": { "tags": [ @@ -2318,6 +3438,204 @@ } ] }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns": { + "type": "object", + "required": [ + "kind", + "apiVersion", + "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": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsSpec" + }, + "status": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsStatus" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "logsdrilldown.grafana.app", + "kind": "LogsDrilldownDefaultColumns", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList": { + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + ] + } + }, + "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": "logsdrilldown.grafana.app", + "kind": "LogsDrilldownDefaultColumnsList", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel" + } + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord": { + "type": "object", + "required": [ + "columns", + "labels" + ], + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "labels": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord" + } + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsOperatorState": { + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsSpec": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsStatus": { + "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.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsOperatorState" + } + } + }, + "additionalProperties": false + }, "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaults": { "type": "object", "required": [ 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 2061616fc9a..4c7c45aad35 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 @@ -3783,18 +3783,29 @@ "type": "object", "required": [ "title", - "content" + "content", + "kind" ], "properties": { "content": { "type": "string" }, + "kind": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind" + }, "title": { "type": "string" } }, "additionalProperties": false }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind": { + "type": "string", + "enum": [ + "grafana", + "mimir" + ] + }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": { "type": "object", "required": [ diff --git a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json index dd1b486823e..dee1a56d6c0 100644 --- a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json @@ -40,7 +40,7 @@ "tags": [ "Playlist" ], - "description": "list objects of kind Playlist", + "description": "list or watch objects of kind Playlist", "operationId": "listPlaylist", "parameters": [ { @@ -1850,6 +1850,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/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index f99b8f60738..d4764b742f1 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -36,6 +36,1126 @@ } } }, + "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections": { + "get": { + "tags": [ + "Connection" + ], + "description": "list or watch objects of kind Connection", + "operationId": "listConnection", + "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.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "post": { + "tags": [ + "Connection" + ], + "description": "create a Connection", + "operationId": "createConnection", + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "delete": { + "tags": [ + "Connection" + ], + "description": "delete collection of Connection", + "operationId": "deletecollectionConnection", + "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": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "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/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}": { + "get": { + "tags": [ + "Connection" + ], + "description": "read the specified Connection", + "operationId": "getConnection", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "put": { + "tags": [ + "Connection" + ], + "description": "replace the specified Connection", + "operationId": "replaceConnection", + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "delete": { + "tags": [ + "Connection" + ], + "description": "delete a Connection", + "operationId": "deleteConnection", + "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": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "patch": { + "tags": [ + "Connection" + ], + "description": "partially update the specified Connection", + "operationId": "updateConnection", + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Connection", + "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/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/status": { + "get": { + "tags": [ + "Connection" + ], + "description": "read status of the specified Connection", + "operationId": "getConnectionStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "put": { + "tags": [ + "Connection" + ], + "description": "replace status of the specified Connection", + "operationId": "replaceConnectionStatus", + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "patch": { + "tags": [ + "Connection" + ], + "description": "partially update status of the specified Connection", + "operationId": "updateConnectionStatus", + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "Connection" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Connection", + "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/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/jobs": { "get": { "tags": [ @@ -3198,6 +4318,19 @@ } } }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.BitbucketConnectionConfig": { + "type": "object", + "required": [ + "clientID" + ], + "properties": { + "clientID": { + "description": "App client ID", + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.BitbucketRepositoryConfig": { "type": "object", "required": [ @@ -3223,6 +4356,227 @@ } } }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.Connection": { + "description": "When this code is changed, make sure to update the code generation. As of writing, this can be done via the hack dir in the root of the repo: ./hack/update-codegen.sh provisioning If you've opened the generated files in this dir at some point in VSCode, you may also have to re-open them to clear errors.", + "type": "object", + "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" + } + ] + }, + "secure": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionSecure" + } + ] + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "provisioning.grafana.app", + "kind": "Connection", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionInfo": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionList": { + "type": "object", + "required": [ + "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.provisioning.pkg.apis.provisioning.v0alpha1.Connection" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "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": "provisioning.grafana.app", + "kind": "ConnectionList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionSecure": { + "type": "object", + "properties": { + "clientSecret": { + "description": "ClientSecret is the reference to the secret used for other providers authentication, and Github on-behalf-of authentication. This value is stored securely and cannot be read back", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue" + } + ] + }, + "privateKey": { + "description": "PrivateKey is the reference to the private key used for GitHub App authentication. This value is stored securely and cannot be read back", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue" + } + ] + }, + "webhook": { + "description": "Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionSpec": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "bitbucket": { + "description": "Bitbucket connection configuration Only applicable when provider is \"bitbucket\"", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.BitbucketConnectionConfig" + } + ] + }, + "github": { + "description": "GitHub connection configuration Only applicable when provider is \"github\"", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.GitHubConnectionConfig" + } + ] + }, + "gitlab": { + "description": "Gitlab connection configuration Only applicable when provider is \"gitlab\"", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.GitlabConnectionConfig" + } + ] + }, + "type": { + "description": "The connection provider type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"github\"`\n - `\"gitlab\"`", + "type": "string", + "default": "", + "enum": [ + "bitbucket", + "github", + "gitlab" + ] + }, + "url": { + "description": "The connection URL", + "type": "string" + } + } + }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionStatus": { + "description": "The status of a Connection. This is expected never to be created by a kubectl call or similar, and is expected to rarely (if ever) be edited manually.", + "type": "object", + "required": [ + "observedGeneration", + "state", + "health" + ], + "properties": { + "health": { + "description": "The connection health status", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.HealthStatus" + } + ] + }, + "observedGeneration": { + "description": "The generation of the spec last time reconciliation ran", + "type": "integer", + "format": "int64", + "default": 0 + }, + "state": { + "description": "Connection state\n\nPossible enum values:\n - `\"connected\"`\n - `\"disconnected\"`", + "type": "string", + "default": "", + "enum": [ + "connected", + "disconnected" + ] + } + } + }, "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.DeleteJobOptions": { "type": "object", "properties": { @@ -3344,6 +4698,25 @@ } } }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.GitHubConnectionConfig": { + "type": "object", + "required": [ + "appID", + "installationID" + ], + "properties": { + "appID": { + "description": "GitHub App ID", + "type": "string", + "default": "" + }, + "installationID": { + "description": "GitHub App installation ID", + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.GitHubRepositoryConfig": { "type": "object", "required": [ @@ -3415,6 +4788,19 @@ } } }, + "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.GitlabConnectionConfig": { + "type": "object", + "required": [ + "clientID" + ], + "properties": { + "clientID": { + "description": "App client ID", + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.HealthStatus": { "type": "object", "required": [ @@ -3696,7 +5082,7 @@ "format": "int64" }, "errors": { - "description": "Report errors for this resource type This may not be an exhaustive list and recommend looking at the logs for more info", + "description": "Report errors/warnings for this resource type This may not be an exhaustive list and recommend looking at the logs for more info", "type": "array", "items": { "type": "string", @@ -3722,6 +5108,18 @@ "type": "integer", "format": "int64" }, + "warning": { + "description": "The error count", + "type": "integer", + "format": "int64" + }, + "warnings": { + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, "write": { "type": "integer", "format": "int64" @@ -3849,6 +5247,13 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.RepositoryURLs" } ] + }, + "warnings": { + "type": "array", + "items": { + "type": "string", + "default": "" + } } } }, @@ -3890,10 +5295,6 @@ "com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.MigrateJobOptions": { "type": "object", "properties": { - "history": { - "description": "Preserve history (if possible)", - "type": "boolean" - }, "message": { "description": "Message to use when committing the changes in a single commit", "type": "string" @@ -4132,6 +5533,14 @@ } ] }, + "connection": { + "description": "The connection the repository references. This means the Repository is interacting with git via a Connection.", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ConnectionInfo" + } + ] + }, "description": { "description": "Repository description", "type": "string" @@ -4415,10 +5824,6 @@ "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" - }, - "legacyStorage": { - "description": "The backend is using legacy storage FIXME: Not sure where this should be exposed... but we need it somewhere The UI should force the onboarding workflow when this is true", - "type": "boolean" } } }, diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index b9d0283e37a..f9d5cdb70e5 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -10,7 +10,9 @@ import ( "k8s.io/apimachinery/pkg/util/version" "k8s.io/client-go/kubernetes" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" @@ -37,6 +39,11 @@ func TestIntegrationOpenAPIs(t *testing.T) { featuremgmt.FlagKubernetesAlertingHistorian, featuremgmt.FlagKubernetesLogsDrilldown, }, + // Explicitly configure with mode 5 the resources supported by provisioning. + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, + "folders.folder.grafana.app": {DualWriterMode: rest.Mode5}, + }, }) t.Run("check valid version response", func(t *testing.T) { diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 057452eb7f9..da9a6530e5b 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -54,47 +54,48 @@ func TestIntegrationPlaylist(t *testing.T) { require.NoError(t, err) // t.Logf("%s", disco) require.JSONEq(t, `[ - { - "version": "v0alpha1", - "freshness": "Current", - "resources": [ - { - "resource": "playlists", - "responseKind": { - "group": "", - "kind": "Playlist", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "playlist", - "subresources": [ - { - "responseKind": { - "group": "", - "kind": "Playlist", - "version": "" - }, - "subresource": "status", - "verbs": [ - "get", - "patch", - "update" - ] - } - ], - "verbs": [ - "create", - "delete", - "deletecollection", - "get", - "list", - "patch", - "update" - ] - } - ] - } - ]`, disco) + { + "freshness": "Current", + "resources": [ + { + "resource": "playlists", + "responseKind": { + "group": "", + "kind": "Playlist", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "playlist", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "Playlist", + "version": "" + }, + "subresource": "status", + "verbs": [ + "get", + "patch", + "update" + ] + } + ], + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + } + ], + "version": "v0alpha1" + } + ]`, disco) }) t.Run("with k8s api flag", func(t *testing.T) { @@ -106,9 +107,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (file, mode 0)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "file", // write the files to disk + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode0, @@ -119,9 +121,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (file, mode 1)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "file", // write the files to disk + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, @@ -132,9 +135,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (file, mode 2)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "file", // write the files to disk + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode2, @@ -145,9 +149,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (file, mode 3)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "file", // write the files to disk + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode3, @@ -158,9 +163,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (file, mode 5)", func(t *testing.T) { helper := doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "file", // write the files to disk + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode5, @@ -198,9 +204,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for unified storage - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, // use the entity api tables + DisableDataMigrations: true, + AppModeProduction: false, // required for unified storage + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, // use the entity api tables UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode0, @@ -211,18 +218,20 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (unified storage, mode 1)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, - EnableFeatureToggles: []string{}, + DisableDataMigrations: true, + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, + EnableFeatureToggles: []string{}, })) }) t.Run("with dual write (unified storage, mode 2)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for unified storage - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, // use the entity api tables + DisableDataMigrations: true, + AppModeProduction: false, // required for unified storage + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, // use the entity api tables UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode2, @@ -233,9 +242,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (unified storage, mode 3)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for unified storage - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, // use the entity api tables + DisableDataMigrations: true, + AppModeProduction: false, // required for unified storage + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, // use the entity api tables UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode3, @@ -246,9 +256,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Run("with dual write (unified storage, mode 5)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for unified storage - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, // use the entity api tables + DisableDataMigrations: true, + AppModeProduction: false, // required for unified storage + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, // use the entity api tables UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode5, @@ -262,9 +273,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Skip("local etcd testing") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode0, @@ -288,9 +300,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Skip("local etcd testing") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, @@ -314,9 +327,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Skip("local etcd testing") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode2, @@ -340,9 +354,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Skip("local etcd testing") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode3, @@ -366,9 +381,10 @@ func TestIntegrationPlaylist(t *testing.T) { t.Skip("local etcd testing") helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeEtcd, // requires etcd running on localhost:2379 UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ RESOURCEGROUP: { DualWriterMode: grafanarest.Mode5, @@ -410,6 +426,45 @@ func doPlaylistTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelp require.Equal(t, metav1.StatusReasonForbidden, rsp.Status.Reason) }) + t.Run("Check CRUD operations with None role", func(t *testing.T) { + // Create a playlist with admin user + clientAdmin := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + created, err := clientAdmin.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/playlist-generate.yaml"), + metav1.CreateOptions{}, + ) + require.NoError(t, err) + + clientNone := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.None, + GVR: gvr, + }) + + // Now check if None user can perform a Get to start a playlist + _, err = clientNone.Resource.Get(context.Background(), created.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + + // None role can get but can not create edit or delete a playlist + _, err = clientNone.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/playlist-generate.yaml"), + metav1.CreateOptions{}, + ) + require.Error(t, err) + + _, err = clientNone.Resource.Update(context.Background(), created, metav1.UpdateOptions{}) + require.Error(t, err) + + err = clientNone.Resource.Delete(context.Background(), created.GetName(), metav1.DeleteOptions{}) + require.Error(t, err) + + // delete created resource + err = clientAdmin.Resource.Delete(context.Background(), created.GetName(), metav1.DeleteOptions{}) + require.NoError(t, err) + }) + t.Run("Check k8s client-go List from different org users", func(t *testing.T) { // Check Org1 Viewer client := helper.GetResourceClient(apis.ResourceClientArgs{ diff --git a/pkg/tests/apis/plugins/discovery_test.go b/pkg/tests/apis/plugins/discovery_test.go index a6e507a630b..1cbcfff487a 100644 --- a/pkg/tests/apis/plugins/discovery_test.go +++ b/pkg/tests/apis/plugins/discovery_test.go @@ -21,19 +21,19 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "freshness": "Current", "resources": [ { - "resource": "pluginmetas", + "resource": "metas", "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "scope": "Namespaced", - "singularResource": "pluginmeta", + "singularResource": "meta", "subresources": [ { "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "subresource": "status", @@ -57,7 +57,7 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "version": "" }, "scope": "Namespaced", - "singularResource": "plugins", + "singularResource": "plugin", "subresources": [ { "responseKind": { diff --git a/pkg/tests/apis/plugins/metas_test.go b/pkg/tests/apis/plugins/metas_test.go new file mode 100644 index 00000000000..0132ee72173 --- /dev/null +++ b/pkg/tests/apis/plugins/metas_test.go @@ -0,0 +1,161 @@ +package plugins + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/util/testutil" +) + +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: gvrPlugins, + }) + + plugin1Name := "test-plugin-metas-1" + plugin1 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "piechart", "version": "1.0.0"} + }`, plugin1Name)) + _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) + require.NoError(t, err) + + plugin2Name := "test-plugin-metas-2" + plugin2 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "table", "version": "1.0.0"} + }`, plugin2Name)) + _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.MetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 2) + + foundIDs := make(map[string]bool) + for _, item := range response.Result.Items { + require.NotNil(t, item.Spec.PluginJson) + foundIDs[item.Spec.PluginJson.Id] = true + require.NotEmpty(t, item.Spec.PluginJson.Id) + require.NotEmpty(t, item.Spec.PluginJson.Type) + require.NotEmpty(t, item.Spec.PluginJson.Name) + } + require.True(t, foundIDs["piechart"]) + require.True(t, foundIDs["table"]) + }) + + t.Run("list plugin metas with no plugins", func(t *testing.T) { + helper := setupHelper(t) + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.MetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 0) + }) + + 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: gvrPlugins, + }) + + pluginName := "test-plugin-meta" + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "piechart", "version": "1.0.0"} + }`, pluginName)) + _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/%s", namespace, pluginName) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.Meta{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Spec.PluginJson) + require.Equal(t, "piechart", response.Result.Spec.PluginJson.Id) + require.NotEmpty(t, response.Result.Spec.PluginJson.Name) + require.NotEmpty(t, response.Result.Spec.PluginJson.Type) + }) + + t.Run("get plugin meta for non-existent plugin", func(t *testing.T) { + helper := setupHelper(t) + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/non-existent-plugin", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.Meta{}) + + require.NotNil(t, response.Status) + require.Equal(t, int32(404), response.Status.Code) + }) + + t.Run("get plugin meta for plugin with non-existent metadata", func(t *testing.T) { + helper := setupHelper(t) + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrPlugins, + }) + + pluginName := "test-plugin-meta-not-found" + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "non-existent-plugin-id", "version": "1.0.0"} + }`, pluginName)) + _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/%s", namespace, pluginName) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.Meta{}) + + require.NotNil(t, response.Status) + require.Equal(t, int32(404), response.Status.Code) + }) +} diff --git a/pkg/tests/apis/plugins/pluginmeta_test.go b/pkg/tests/apis/plugins/pluginmeta_test.go deleted file mode 100644 index af70f02a6f8..00000000000 --- a/pkg/tests/apis/plugins/pluginmeta_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestIntegrationPluginMeta(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - 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: gvrPlugins, - }) - - pluginName := "test-plugin-meta" - plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} - }`, pluginName)) - _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) - require.NoError(t, err) - - namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMeta{}) - - require.NotNil(t, response.Result) - require.NotNil(t, response.Result.Spec.PluginJSON) - require.Equal(t, "grafana-piechart-panel", response.Result.Spec.PluginJSON.Id) - require.NotEmpty(t, response.Result.Spec.PluginJSON.Name) - require.NotEmpty(t, response.Result.Spec.PluginJSON.Type) - }) - - t.Run("get plugin meta for non-existent plugin", func(t *testing.T) { - helper := setupHelper(t) - namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/non-existent-plugin", namespace) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMeta{}) - - require.NotNil(t, response.Status) - require.Equal(t, int32(404), response.Status.Code) - }) - - t.Run("get plugin meta for plugin with non-existent metadata", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPlugins, - }) - - pluginName := "test-plugin-meta-not-found" - plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "non-existent-plugin-id", "version": "1.0.0"} - }`, pluginName)) - _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) - require.NoError(t, err) - - namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMeta{}) - - require.NotNil(t, response.Status) - require.Equal(t, int32(404), response.Status.Code) - }) -} diff --git a/pkg/tests/apis/plugins/pluginmetas_test.go b/pkg/tests/apis/plugins/pluginmetas_test.go deleted file mode 100644 index 1ffe8d35a50..00000000000 --- a/pkg/tests/apis/plugins/pluginmetas_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestIntegrationPluginMetas(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: gvrPlugins, - }) - - plugin1Name := "test-plugin-metas-1" - plugin1 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} - }`, plugin1Name)) - _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) - require.NoError(t, err) - - plugin2Name := "test-plugin-metas-2" - plugin2 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "grafana-clock-panel", "version": "1.0.0"} - }`, plugin2Name)) - _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) - require.NoError(t, err) - - namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMetaList{}) - - require.NotNil(t, response.Result) - require.NotNil(t, response.Result.Items) - require.GreaterOrEqual(t, len(response.Result.Items), 2) - - foundIDs := make(map[string]bool) - for _, item := range response.Result.Items { - require.NotNil(t, item.Spec.PluginJSON) - foundIDs[item.Spec.PluginJSON.Id] = true - require.NotEmpty(t, item.Spec.PluginJSON.Id) - require.NotEmpty(t, item.Spec.PluginJSON.Type) - require.NotEmpty(t, item.Spec.PluginJSON.Name) - } - require.True(t, foundIDs["grafana-piechart-panel"]) - require.True(t, foundIDs["grafana-clock-panel"]) - }) - - t.Run("list plugin metas with no plugins", func(t *testing.T) { - helper := setupHelper(t) - namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMetaList{}) - - require.NotNil(t, response.Result) - require.NotNil(t, response.Result.Items) - require.GreaterOrEqual(t, len(response.Result.Items), 0) - }) -} diff --git a/pkg/tests/apis/plugins/plugininstalls_test.go b/pkg/tests/apis/plugins/plugins_test.go similarity index 100% rename from pkg/tests/apis/plugins/plugininstalls_test.go rename to pkg/tests/apis/plugins/plugins_test.go diff --git a/pkg/tests/apis/provisioning/connection_status_auth_test.go b/pkg/tests/apis/provisioning/connection_status_auth_test.go new file mode 100644 index 00000000000..fbddd85999a --- /dev/null +++ b/pkg/tests/apis/provisioning/connection_status_auth_test.go @@ -0,0 +1,88 @@ +package provisioning + +import ( + "context" + "net/http" + "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" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_ConnectionStatusAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + + // Create a connection for testing + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection-status-test", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.NoError(t, err, "failed to create connection") + + t.Run("admin can GET connection status", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET connection status") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET connection status", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET connection status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET connection status", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET connection status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} diff --git a/pkg/tests/apis/provisioning/connection_test.go b/pkg/tests/apis/provisioning/connection_test.go new file mode 100644 index 00000000000..ea28ac88359 --- /dev/null +++ b/pkg/tests/apis/provisioning/connection_test.go @@ -0,0 +1,413 @@ +package provisioning + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana/pkg/util/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + t.Run("should perform CRUDL requests on connection", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + // CREATE + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.NoError(t, err, "failed to create resource") + + // READ + output, err := helper.Connections.Resource.Get(ctx, "connection", metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + assert.Equal(t, "connection", output.GetName(), "name should be equal") + assert.Equal(t, "default", output.GetNamespace(), "namespace should be equal") + spec := output.Object["spec"].(map[string]any) + assert.Equal(t, "github", spec["type"], "type should be equal") + assert.Equal(t, "https://github.com/settings/installations/454545", spec["url"], "url should be equal") + require.Contains(t, spec, "github") + githubInfo := spec["github"].(map[string]any) + assert.Equal(t, "123456", githubInfo["appID"], "appID should be equal") + assert.Equal(t, "454545", githubInfo["installationID"], "installationID should be equal") + require.Contains(t, output.Object, "secure", "object should contain secure") + assert.Contains(t, output.Object["secure"], "privateKey", "secure should contain PrivateKey") + + // LIST + list, err := helper.Connections.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "failed to list resource") + assert.Equal(t, 1, len(list.Items), "should have one connection") + assert.Equal(t, "connection", list.Items[0].GetName(), "name should be equal") + + // UPDATE + updatedConnection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "456789", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + res, err := helper.Connections.Resource.Update(ctx, updatedConnection, metav1.UpdateOptions{}) + require.NoError(t, err, "failed to update resource") + spec = res.Object["spec"].(map[string]any) + require.Contains(t, spec, "github") + githubInfo = spec["github"].(map[string]any) + assert.Equal(t, "456789", githubInfo["appID"], "appID should be updated") + + // DELETE + require.NoError(t, helper.Connections.Resource.Delete(ctx, "connection", metav1.DeleteOptions{}), "failed to delete resource") + list, err = helper.Connections.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "failed to list resources") + assert.Equal(t, 0, len(list.Items), "should have no connections") + }) + + t.Run("viewer can't create or get connection", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + + result := helper.ViewerREST.Post(). + Namespace("default"). + Resource("connections"). + Body(connection). + Do(t.Context()) + + require.NotNil(t, result.Error()) + err := &k8serrors.StatusError{} + require.True(t, errors.As(result.Error(), &err)) + assert.Equal(t, metav1.StatusReasonForbidden, err.Status().Reason) + assert.Contains(t, err.Status().Message, "User \"viewer\" cannot create resource \"connections\"") + assert.Contains(t, err.Status().Message, "admin role is required") + + result = helper.ViewerREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection"). + Do(t.Context()) + require.NotNil(t, result.Error()) + err = &k8serrors.StatusError{} + require.True(t, errors.As(result.Error(), &err)) + assert.Equal(t, metav1.StatusReasonForbidden, err.Status().Reason) + assert.Contains(t, err.Status().Message, "User \"viewer\" cannot get resource \"connections\"") + assert.Contains(t, err.Status().Message, "admin role is required") + }) +} + +func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) { + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + t.Run("should fail when type is empty", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "", + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "type must be specified") + }) + + t.Run("should fail when type is invalid", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "some-invalid-type", + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "spec.type: Unsupported value: \"some-invalid-type\"") + }) + + t.Run("should fail when type is github but 'github' field is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "github info must be specified for GitHub connection") + }) + + t.Run("should fail when type is github but private key is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "privateKey must be specified for GitHub connection") + }) + + t.Run("should fail when type is github but a client Secret is specified", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + "clientSecret": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "clientSecret is forbidden in GitHub connection") + }) + + t.Run("should fail when type is bitbucket but 'bitbucket' field is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "bitbucket", + }, + "secure": map[string]any{ + "clientSecret": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "bitbucket info must be specified in Bitbucket connection") + }) + + t.Run("should fail when type is bitbucket but client secret is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "bitbucket", + "bitbucket": map[string]any{ + "clientID": "123456", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "clientSecret must be specified for Bitbucket connection") + }) + + t.Run("should fail when type is bitbucket but a private key is specified", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "bitbucket", + "bitbucket": map[string]any{ + "clientID": "123456", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + "clientSecret": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "privateKey is forbidden in Bitbucket connection") + }) + + t.Run("should fail when type is gitlab but 'gitlab' field is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "gitlab", + }, + "secure": map[string]any{ + "clientSecret": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "gitlab info must be specified in Gitlab connection") + }) + + t.Run("should fail when type is gitlab but client secret is not there", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "gitlab", + "gitlab": map[string]any{ + "clientID": "123456", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "clientSecret must be specified for Gitlab connection") + }) + + t.Run("should fail when type is gitlab but a private key is specified", func(t *testing.T) { + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "gitlab", + "gitlab": map[string]any{ + "clientID": "123456", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + "clientSecret": map[string]any{ + "create": "someSecret", + }, + }, + }} + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.Error(t, err, "failed to create resource") + assert.Contains(t, err.Error(), "privateKey is forbidden in Gitlab connection") + }) +} diff --git a/pkg/tests/apis/provisioning/exportjob_test.go b/pkg/tests/apis/provisioning/exportjob_test.go index 2218e87ef77..5d4348d23cb 100644 --- a/pkg/tests/apis/provisioning/exportjob_test.go +++ b/pkg/tests/apis/provisioning/exportjob_test.go @@ -44,6 +44,7 @@ func TestIntegrationProvisioning_ExportUnifiedToRepository(t *testing.T) { const repo = "local-repository" testRepo := TestRepo{ Name: repo, + Target: "instance", // Export is only supported for instance sync Copies: map[string]string{}, // No initial files needed for export test ExpectedDashboards: 4, // 4 dashboards created above (v0, v1, v2alpha1, v2beta1) ExpectedFolders: 0, // No folders expected after sync @@ -177,6 +178,7 @@ func TestIntegrationProvisioning_ExportDashboardsWithStoredVersions(t *testing.T const repo = "version-test-repository" testRepo := TestRepo{ Name: repo, + Target: "instance", // Export is only supported for instance sync Copies: map[string]string{}, ExpectedDashboards: len(tests), ExpectedFolders: 0, diff --git a/pkg/tests/apis/provisioning/fieldselector_test.go b/pkg/tests/apis/provisioning/fieldselector_test.go new file mode 100644 index 00000000000..78f05eeacb7 --- /dev/null +++ b/pkg/tests/apis/provisioning/fieldselector_test.go @@ -0,0 +1,211 @@ +package provisioning + +import ( + "context" + "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" +) + +// TestIntegrationProvisioning_RepositoryFieldSelector tests that fieldSelector +// works correctly for Repository resources. This prevents regression where +// fieldSelector=metadata.name= was not working properly. +func TestIntegrationProvisioning_RepositoryFieldSelector(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + // Create multiple repositories for testing + repo1Name := "repo-selector-test-1" + repo2Name := "repo-selector-test-2" + repo3Name := "repo-selector-test-3" + + // Create first repository + repo1 := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo1Name, + "SyncEnabled": false, // Disable sync to speed up test + }) + _, err := helper.Repositories.Resource.Create(ctx, repo1, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create first repository") + helper.WaitForHealthyRepository(t, repo1Name) + + // Create second repository + repo2 := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo2Name, + "SyncEnabled": false, + }) + _, err = helper.Repositories.Resource.Create(ctx, repo2, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create second repository") + helper.WaitForHealthyRepository(t, repo2Name) + + // Create third repository + repo3 := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo3Name, + "SyncEnabled": false, + }) + _, err = helper.Repositories.Resource.Create(ctx, repo3, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create third repository") + helper.WaitForHealthyRepository(t, repo3Name) + + // Verify all repositories were created + allRepos, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "should be able to list all repositories") + require.GreaterOrEqual(t, len(allRepos.Items), 3, "should have at least 3 repositories") + + t.Run("should filter by metadata.name and return single repository", func(t *testing.T) { + list, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=" + repo2Name, + }) + require.NoError(t, err, "fieldSelector query should succeed") + require.Len(t, list.Items, 1, "should return exactly one repository") + require.Equal(t, repo2Name, list.Items[0].GetName(), "should return the correct repository") + }) + + t.Run("should filter by different metadata.name", func(t *testing.T) { + list, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=" + repo1Name, + }) + require.NoError(t, err, "fieldSelector query should succeed") + require.Len(t, list.Items, 1, "should return exactly one repository") + require.Equal(t, repo1Name, list.Items[0].GetName(), "should return the first repository") + }) + + t.Run("should return empty when fieldSelector does not match any repository", func(t *testing.T) { + list, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=non-existent-repository", + }) + require.NoError(t, err, "fieldSelector query should succeed even with no matches") + require.Empty(t, list.Items, "should return empty list when no repositories match") + }) + + t.Run("listing without fieldSelector should return all repositories", func(t *testing.T) { + list, err := helper.Repositories.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "should be able to list without fieldSelector") + require.GreaterOrEqual(t, len(list.Items), 3, "should return all repositories when no filter is applied") + + // Verify our test repositories are in the list + names := make(map[string]bool) + for _, item := range list.Items { + names[item.GetName()] = true + } + require.True(t, names[repo1Name], "should contain repo1") + require.True(t, names[repo2Name], "should contain repo2") + require.True(t, names[repo3Name], "should contain repo3") + }) +} + +// TestIntegrationProvisioning_JobFieldSelector tests that fieldSelector +// works correctly for Job resources. This prevents regression where +// fieldSelector=metadata.name= was not working properly. +func TestIntegrationProvisioning_JobFieldSelector(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + // Create a repository to trigger jobs + repoName := "job-selector-test-repo" + repo := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repoName, + "SyncEnabled": false, + }) + _, err := helper.Repositories.Resource.Create(ctx, repo, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create repository") + helper.WaitForHealthyRepository(t, repoName) + + // Copy some test files to trigger jobs + helper.CopyToProvisioningPath(t, "testdata/all-panels.json", "job-test-dashboard-1.json") + helper.CopyToProvisioningPath(t, "testdata/text-options.json", "job-test-dashboard-2.json") + + // Trigger multiple jobs to have multiple job resources + job1Spec := provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{}, + } + + // Trigger first job + body1 := asJSON(job1Spec) + result1 := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repoName). + SubResource("jobs"). + Body(body1). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result1.Error(), "should be able to trigger first job") + + obj1, err := result1.Get() + require.NoError(t, err, "should get first job object") + job1 := obj1.(*unstructured.Unstructured) + job1Name := job1.GetName() + require.NotEmpty(t, job1Name, "first job should have a name") + + // Wait for first job to complete before starting second + helper.AwaitJobs(t, repoName) + + // Trigger second job + result2 := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repoName). + SubResource("jobs"). + Body(body1). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result2.Error(), "should be able to trigger second job") + + obj2, err := result2.Get() + require.NoError(t, err, "should get second job object") + job2 := obj2.(*unstructured.Unstructured) + job2Name := job2.GetName() + require.NotEmpty(t, job2Name, "second job should have a name") + + t.Run("should filter by metadata.name and return single job", func(t *testing.T) { + // Note: Jobs are ephemeral and may complete quickly, so we test while they exist + list, err := helper.Jobs.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=" + job2Name, + }) + require.NoError(t, err, "fieldSelector query should succeed") + + // The job might have completed already, but if it exists, it should be the only one + if len(list.Items) > 0 { + require.Len(t, list.Items, 1, "should return at most one job") + require.Equal(t, job2Name, list.Items[0].GetName(), "should return the correct job") + } + }) + + t.Run("should filter by different metadata.name", func(t *testing.T) { + list, err := helper.Jobs.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=" + job1Name, + }) + require.NoError(t, err, "fieldSelector query should succeed") + + // The job might have completed already, but if it exists, it should be the only one + if len(list.Items) > 0 { + require.Len(t, list.Items, 1, "should return at most one job") + require.Equal(t, job1Name, list.Items[0].GetName(), "should return the first job") + } + }) + + t.Run("should return empty when fieldSelector does not match any job", func(t *testing.T) { + list, err := helper.Jobs.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: "metadata.name=non-existent-job", + }) + require.NoError(t, err, "fieldSelector query should succeed even with no matches") + require.Empty(t, list.Items, "should return empty list when no jobs match") + }) + + t.Run("listing without fieldSelector should work", func(t *testing.T) { + list, err := helper.Jobs.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "should be able to list without fieldSelector") + // Jobs may have completed, so we don't assert on count, just that the query works + t.Logf("Found %d active jobs without filter", len(list.Items)) + }) +} diff --git a/pkg/tests/apis/provisioning/files_test.go b/pkg/tests/apis/provisioning/files_test.go index 3eed9171578..e31674be505 100644 --- a/pkg/tests/apis/provisioning/files_test.go +++ b/pkg/tests/apis/provisioning/files_test.go @@ -1,7 +1,9 @@ package provisioning import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -11,6 +13,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/util/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -68,22 +71,45 @@ func TestIntegrationProvisioning_DeleteResources(t *testing.T) { helper.validateManagedDashboardsFolderMetadata(t, ctx, repo, dashboards.Items) - t.Run("delete individual dashboard file, should delete from repo and grafana", func(t *testing.T) { + t.Run("delete individual dashboard file on configured branch should succeed", func(t *testing.T) { result := helper.AdminREST.Delete(). Namespace("default"). Resource("repositories"). Name(repo). SubResource("files", "dashboard1.json"). Do(ctx) - require.NoError(t, result.Error()) - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "dashboard1.json") - require.Error(t, err) - dashboards, err = helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.Equal(t, 2, len(dashboards.Items)) + require.NoError(t, result.Error(), "delete file on configured branch should succeed") + + // Verify the dashboard is removed from Grafana + const allPanelsUID = "n1jR8vnnz" // UID from all-panels.json + _, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) + require.Error(t, err, "dashboard should be deleted from Grafana") + require.True(t, apierrors.IsNotFound(err), "should return NotFound for deleted dashboard") }) - t.Run("delete folder, should delete from repo and grafana all nested resources too", func(t *testing.T) { + t.Run("delete individual dashboard file on branch should succeed", func(t *testing.T) { + // Create a branch first by creating a file on a branch + branchRef := "test-branch-delete" + helper.CopyToProvisioningPath(t, "testdata/text-options.json", "branch-test-delete.json") + + // Delete on branch should work + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "branch-test-delete.json"). + Param("ref", branchRef). + Do(ctx) + // Note: This might fail if branch doesn't exist, but the important thing is it doesn't return MethodNotAllowed + if result.Error() != nil { + var statusErr *apierrors.StatusError + if errors.As(result.Error(), &statusErr) { + require.NotEqual(t, int32(http.StatusMethodNotAllowed), statusErr.ErrStatus.Code, "should not return MethodNotAllowed for branch delete") + } + } + }) + + t.Run("delete folder on configured branch should return MethodNotAllowed", func(t *testing.T) { // need to delete directly through the url, because the k8s client doesn't support `/` in a subresource // but that is needed by gitsync to know that it is a folder addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() @@ -94,27 +120,11 @@ func TestIntegrationProvisioning_DeleteResources(t *testing.T) { require.NoError(t, err) // nolint:errcheck defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "should return MethodNotAllowed for configured branch folder delete") - // should be deleted from the repo - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder") - require.Error(t, err) + // Verify a file inside the folder still exists (operation was rejected) _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "dashboard2.json") - require.Error(t, err) - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested") - require.Error(t, err) - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested", "dashboard3.json") - require.Error(t, err) - - // all should be deleted from grafana - for _, d := range dashboards.Items { - _, err = helper.DashboardsV1.Resource.Get(ctx, d.GetName(), metav1.GetOptions{}) - require.Error(t, err) - } - for _, f := range folders.Items { - _, err = helper.Folders.Resource.Get(ctx, f.GetName(), metav1.GetOptions{}) - require.Error(t, err) - } + require.NoError(t, err, "file inside folder should still exist after rejected delete") }) t.Run("deleting a non-existent file should fail", func(t *testing.T) { @@ -158,10 +168,10 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { require.NoError(t, err, "original dashboard should exist in Grafana") require.Equal(t, repo, obj.GetAnnotations()[utils.AnnoKeyManagerIdentity]) - t.Run("move file without content change", func(t *testing.T) { + t.Run("move file without content change on configured branch should succeed", func(t *testing.T) { const targetPath = "moved/simple-move.json" - // Perform the move operation using helper function + // Perform the move operation using helper function (no ref = configured branch) resp := helper.postFilesRequest(t, repo, filesPostOptions{ targetPath: targetPath, originalPath: "all-panels.json", @@ -169,32 +179,52 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { }) // nolint:errcheck defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode, "move operation should succeed") + require.Equal(t, http.StatusOK, resp.StatusCode, "move operation on configured branch should succeed") - // Verify the file moved in the repository - movedObj, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move.json") - require.NoError(t, err, "moved file should exist in repository") + // Verify file was moved - read from new location + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move.json") + require.NoError(t, err, "file should exist at new location") - // Check the content is preserved (verify it's still the all-panels dashboard) - resource, _, err := unstructured.NestedMap(movedObj.Object, "resource") - require.NoError(t, err) - dryRun, _, err := unstructured.NestedMap(resource, "dryRun") - require.NoError(t, err) - title, _, err := unstructured.NestedString(dryRun, "spec", "title") - require.NoError(t, err) - require.Equal(t, "Panel tests - All panels", title, "content should be preserved") - - // Verify original file no longer exists + // Verify file no longer exists at old location _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "all-panels.json") - require.Error(t, err, "original file should no longer exist") - - // Verify dashboard still exists in Grafana with same content but may have updated path references - helper.SyncAndWait(t, repo, nil) - _, err = helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) - require.NoError(t, err, "dashboard should still exist in Grafana after move") + require.Error(t, err, "file should not exist at old location") }) - t.Run("move file to nested path without ref", func(t *testing.T) { + t.Run("move file without content change on branch should succeed", func(t *testing.T) { + const targetPath = "moved/simple-move-branch.json" + branchRef := "test-branch-move" + + // Perform the move operation using helper function with ref parameter + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: targetPath, + originalPath: "all-panels.json", + message: "move file without content change", + ref: branchRef, + }) + // nolint:errcheck + defer resp.Body.Close() + // Note: This might fail if branch doesn't exist, but the important thing is it doesn't return MethodNotAllowed + if resp.StatusCode == http.StatusMethodNotAllowed { + t.Fatal("should not return MethodNotAllowed for branch move") + } + + // If move succeeded (not MethodNotAllowed), verify the file moved in the repository + if resp.StatusCode == http.StatusOK { + movedObj, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move-branch.json") + require.NoError(t, err, "moved file should exist in repository") + + // Check the content is preserved (verify it's still the all-panels dashboard) + resource, _, err := unstructured.NestedMap(movedObj.Object, "resource") + require.NoError(t, err) + dryRun, _, err := unstructured.NestedMap(resource, "dryRun") + require.NoError(t, err) + title, _, err := unstructured.NestedString(dryRun, "spec", "title") + require.NoError(t, err) + require.Equal(t, "Panel tests - All panels", title, "content should be preserved") + } + }) + + t.Run("move file to nested path on configured branch should succeed", func(t *testing.T) { // Test a different scenario: Move a file that was never synced to Grafana // This might reveal the issue if dashboard creation fails during move const sourceFile = "never-synced.json" @@ -203,7 +233,7 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { // DO NOT sync - move the file immediately without it ever being in Grafana const targetPath = "deep/nested/timeline.json" - // Perform the move operation without the file ever being synced to Grafana + // Perform the move operation without the file ever being synced to Grafana (no ref = configured branch) resp := helper.postFilesRequest(t, repo, filesPostOptions{ targetPath: targetPath, originalPath: sourceFile, @@ -211,70 +241,25 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { }) // nolint:errcheck defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode, "move operation should succeed") + require.Equal(t, http.StatusOK, resp.StatusCode, "move operation on configured branch should succeed") - // Check folders were created and validate hierarchy - folderList, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err, "should be able to list folders") + // File should exist at new location + _, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "deep", "nested", "timeline.json") + require.NoError(t, err, "file should exist at new nested location") - // Build a map of folder names to their objects for easier lookup - folders := make(map[string]*unstructured.Unstructured) - for _, folder := range folderList.Items { - title, _, _ := unstructured.NestedString(folder.Object, "spec", "title") - folders[title] = &folder - parent, _, _ := unstructured.NestedString(folder.Object, "metadata", "annotations", "grafana.app/folder") - t.Logf(" - %s: %s (parent: %s)", folder.GetName(), title, parent) - } - - // Validate expected folders exist with proper hierarchy - // Expected structure: deep -> deep/nested - deepFolderTitle := "deep" - nestedFolderTitle := "nested" - - // Validate "deep" folder exists and has no parent (is top-level) - require.Contains(t, folders, deepFolderTitle, "deep folder should exist") - f := folders[deepFolderTitle] - deepFolderName := f.GetName() - title, _, _ := unstructured.NestedString(f.Object, "spec", "title") - require.Equal(t, deepFolderTitle, title, "deep folder should have correct title") - parent, found, _ := unstructured.NestedString(f.Object, "metadata", "annotations", "grafana.app/folder") - require.True(t, !found || parent == "", "deep folder should be top-level (no parent)") - - // Validate "deep/nested" folder exists and has "deep" as parent - require.Contains(t, folders, nestedFolderTitle, "nested folder should exist") - f = folders[nestedFolderTitle] - nestedFolderName := f.GetName() - title, _, _ = unstructured.NestedString(f.Object, "spec", "title") - require.Equal(t, nestedFolderTitle, title, "nested folder should have correct title") - parent, _, _ = unstructured.NestedString(f.Object, "metadata", "annotations", "grafana.app/folder") - require.Equal(t, deepFolderName, parent, "nested folder should have deep folder as parent") - - // The key test: Check if dashboard was created in Grafana during move - const timelineUID = "mIJjFy8Kz" - dashboard, err := helper.DashboardsV1.Resource.Get(ctx, timelineUID, metav1.GetOptions{}) - require.NoError(t, err, "dashboard should exist in Grafana after moving never-synced file") - dashboardFolder, _, _ := unstructured.NestedString(dashboard.Object, "metadata", "annotations", "grafana.app/folder") - - // Validate dashboard is in the correct nested folder - require.Equal(t, nestedFolderName, dashboardFolder, "dashboard should be in the nested folder") - - // Verify the file moved in the repository - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "deep", "nested", "timeline.json") - require.NoError(t, err, "moved file should exist in nested repository path") - - // Verify the original file no longer exists in the repository + // File should not exist at original location _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", sourceFile) - require.Error(t, err, "original file should no longer exist in repository") + require.Error(t, err, "file should not exist at original location after move") }) - t.Run("move file with content update", func(t *testing.T) { - const sourcePath = "moved/simple-move.json" // Use the file from previous test + t.Run("move file with content update on configured branch should succeed", func(t *testing.T) { + const sourcePath = "moved/simple-move.json" // Use the file we moved earlier const targetPath = "updated/content-updated.json" // Use text-options.json content for the update updatedContent := helper.LoadFile("testdata/text-options.json") - // Perform move with content update using helper function + // Perform move with content update using helper function (no ref = configured branch) resp := helper.postFilesRequest(t, repo, filesPostOptions{ targetPath: targetPath, originalPath: sourcePath, @@ -283,51 +268,27 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { }) // nolint:errcheck defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode, "move with content update should succeed") + require.Equal(t, http.StatusOK, resp.StatusCode, "move with content update on configured branch should succeed") - // Verify the moved file has updated content (should now be text-options dashboard) + // File should exist at new location with updated content movedObj, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "updated", "content-updated.json") - require.NoError(t, err, "moved file should exist in repository") + require.NoError(t, err, "file should exist at new location") + // Verify content was updated (should be text-options dashboard now) resource, _, err := unstructured.NestedMap(movedObj.Object, "resource") require.NoError(t, err) dryRun, _, err := unstructured.NestedMap(resource, "dryRun") require.NoError(t, err) title, _, err := unstructured.NestedString(dryRun, "spec", "title") require.NoError(t, err) - require.Equal(t, "Text options", title, "content should be updated to text-options dashboard") + require.Equal(t, "Text options", title, "content should be updated") - // Check it has the expected UID from text-options.json - name, _, err := unstructured.NestedString(dryRun, "metadata", "name") - require.NoError(t, err) - require.Equal(t, "WZ7AhQiVz", name, "should have the UID from text-options.json") - - // Verify source file no longer exists - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move.json") - require.Error(t, err, "source file should no longer exist") - - // Sync and verify the updated dashboard exists in Grafana - helper.SyncAndWait(t, repo, nil) - const textOptionsUID = "WZ7AhQiVz" // UID from text-options.json - updatedDashboard, err := helper.DashboardsV1.Resource.Get(ctx, textOptionsUID, metav1.GetOptions{}) - require.NoError(t, err, "updated dashboard should exist in Grafana") - - // Verify the original dashboard was deleted from Grafana - _, err = helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) - require.Error(t, err, "original dashboard should be deleted from Grafana") - require.True(t, apierrors.IsNotFound(err)) - - // Verify the new dashboard has the updated content - updatedTitle, _, err := unstructured.NestedString(updatedDashboard.Object, "spec", "title") - require.NoError(t, err) - require.Equal(t, "Text options", updatedTitle) + // Source file should not exist anymore + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", sourcePath) + require.Error(t, err, "source file should not exist after move") }) - t.Run("move directory", func(t *testing.T) { - t.Skip("Skip as implementation is broken and leaves dashboards behind in the move") - // FIXME: https://github.com/grafana/git-ui-sync-project/issues/379 - // The current implementation of moving directories is flawed. - // It will be deprecated in favor of queuing a move job + t.Run("move directory on configured branch should return MethodNotAllowed", func(t *testing.T) { // Create some files in a directory first using existing testdata files helper.CopyToProvisioningPath(t, "testdata/timeline-demo.json", "source-dir/timeline-demo.json") helper.CopyToProvisioningPath(t, "testdata/text-options.json", "source-dir/text-options.json") @@ -338,7 +299,7 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { const sourceDir = "source-dir/" const targetDir = "moved-dir/" - // Move directory using helper function + // Move directory using helper function (no ref = configured branch) resp := helper.postFilesRequest(t, repo, filesPostOptions{ targetPath: targetDir, originalPath: sourceDir, @@ -346,20 +307,11 @@ func TestIntegrationProvisioning_MoveResources(t *testing.T) { }) // nolint:errcheck defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - require.NoError(t, err, "should read response body") - t.Logf("Response Body: %s", string(body)) - require.Equal(t, http.StatusOK, resp.StatusCode, "directory move should succeed") + require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "directory move on configured branch should return MethodNotAllowed") - // Verify source directory no longer exists - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "source-dir") - require.Error(t, err, "source directory should no longer exist") - - // Verify target directory and files exist - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved-dir", "timeline-demo.json") - require.NoError(t, err, "moved timeline-demo.json should exist") - _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved-dir", "text-options.json") - require.NoError(t, err, "moved text-options.json should exist") + // Verify files in source directory still exist (operation was rejected) + _, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "source-dir", "timeline-demo.json") + require.NoError(t, err, "file in source directory should still exist after rejected move") }) t.Run("error cases", func(t *testing.T) { @@ -566,7 +518,7 @@ func TestIntegrationProvisioning_FilesOwnershipProtection(t *testing.T) { }) t.Run("DELETE resource owned by different repository - should fail", func(t *testing.T) { - // Create a file manually in the second repo which is already in first one + // Create a file manually in the second repo which has UID from first repo helper.CopyToProvisioningPath(t, "testdata/all-panels.json", "repo2/conflicting-delete.json") printFileTree(t, helper.ProvisioningPath) @@ -590,10 +542,7 @@ func TestIntegrationProvisioning_FilesOwnershipProtection(t *testing.T) { } // Verify it returns BadRequest (400) for ownership conflicts - if !apierrors.IsBadRequest(err) { - t.Errorf("Expected BadRequest error but got: %T - %v", err, err) - return - } + require.True(t, apierrors.IsBadRequest(err), "Expected BadRequest error but got: %T - %v", err, err) // Check error message contains ownership conflict information errorMsg := err.Error() @@ -607,7 +556,7 @@ func TestIntegrationProvisioning_FilesOwnershipProtection(t *testing.T) { targetPath: "moved-dashboard.json", originalPath: path.Join("dashboard2.json"), message: "attempt to move file from different repository", - body: string(helper.LoadFile("testdata/all-panels.json")), // Content to move with the conflicting UID + body: string(helper.LoadFile("testdata/all-panels.json")), // Content with the conflicting UID }) // nolint:errcheck defer resp.Body.Close() @@ -644,3 +593,390 @@ func TestIntegrationProvisioning_FilesOwnershipProtection(t *testing.T) { require.Equal(t, repo2, dashboard2.GetAnnotations()[utils.AnnoKeyManagerIdentity], "repo2's dashboard should still be owned by repo2") }) } + +func TestIntegrationProvisioning_FilesAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "auth-test-repo" + helper.CreateRepo(t, TestRepo{ + Name: repo, + Path: helper.ProvisioningPath, + Target: "instance", + Copies: map[string]string{ + "testdata/all-panels.json": "dashboard1.json", + }, + ExpectedDashboards: 1, + ExpectedFolders: 0, + }) + + // Wait for initial sync to complete + var dashboardUID string + require.EventuallyWithT(t, func(collect *assert.CollectT) { + dashboards, err := helper.DashboardsV1.Resource.List(t.Context(), metav1.ListOptions{}) + if err != nil { + collect.Errorf("could not list dashboards error: %s", err.Error()) + return + } + if len(dashboards.Items) != 1 { + collect.Errorf("should have the expected dashboards after sync. got: %d. expected: %d", len(dashboards.Items), 1) + return + } + assert.Len(collect, dashboards.Items, 1) + dashboardUID = dashboards.Items[0].GetName() + }, waitTimeoutDefault, waitIntervalDefault, "should have the expected dashboards after sync") + + // Grant permissions to Editor user for all dashboards using wildcard + // The access checker checks resource-level permissions, so we need to grant them + // Using wildcard "*" to grant permissions to all dashboards (including ones created during tests) + // Note: Viewer role gets permissions via HTTP API below, Editor gets them here via SetPermissions + helper.SetPermissions(helper.Org1.Editor, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{"dashboards:read", "dashboards:write", "dashboards:delete"}, + Resource: "dashboards", + ResourceAttribute: "uid", + ResourceID: "*", + }, + }) + + // Grant view permission to Viewer role via HTTP API (for the initial dashboard) + // Note: This only grants permissions to the initial dashboard, but viewers should be able to read all + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + setDashboardPermissions := func(permissions []map[string]interface{}) { + payload := map[string]interface{}{ + "items": permissions, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + url := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s/permissions", addr, dashboardUID) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payloadBytes)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + } + + // Grant view permission to Viewer role for the initial dashboard + setDashboardPermissions([]map[string]interface{}{ + {"role": "Viewer", "permission": 1}, // View permission + }) + + t.Run("GET operations", func(t *testing.T) { + t.Run("viewer can GET files", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "viewer should be able to GET files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor can GET files", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to GET files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("admin can GET files", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + }) + + t.Run("POST operations", func(t *testing.T) { + t.Run("viewer cannot POST files", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "viewer-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to POST files") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("editor can POST files", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "editor-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to POST files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + + // Clean up + helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "editor-test.json"). + Do(ctx) + }) + + t.Run("admin can POST files", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "admin-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to POST files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + + // Clean up + helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "admin-test.json"). + Do(ctx) + }) + }) + + t.Run("PUT operations", func(t *testing.T) { + // Create a test file first using admin + helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "update-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + t.Run("viewer cannot PUT files", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Put(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "update-test.json"). + Body(helper.LoadFile("testdata/timeline-demo.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to PUT files") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("editor can PUT files", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Put(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "update-test.json"). + Body(helper.LoadFile("testdata/timeline-demo.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to PUT files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("admin can PUT files", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Put(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "update-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to PUT files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + // Clean up + helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "update-test.json"). + Do(ctx) + }) + + t.Run("DELETE operations", func(t *testing.T) { + // Create test files for deletion tests + helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-viewer-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-editor-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-admin-test.json"). + Body(helper.LoadFile("testdata/text-options.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + t.Run("viewer cannot DELETE files", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-viewer-test.json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to DELETE files") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + + // Verify file still exists + _, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "delete-viewer-test.json") + require.NoError(t, err, "file should still exist after failed delete") + }) + + t.Run("editor can DELETE files", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-editor-test.json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to DELETE files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + + // Verify file was deleted + _, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "delete-editor-test.json") + require.Error(t, err, "file should be deleted") + require.True(t, apierrors.IsNotFound(err), "should return NotFound for deleted file") + }) + + t.Run("admin can DELETE files", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "delete-admin-test.json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to DELETE files") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + + // Verify file was deleted + _, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "delete-admin-test.json") + require.Error(t, err, "file should be deleted") + require.True(t, apierrors.IsNotFound(err), "should return NotFound for deleted file") + }) + }) + + t.Run("folder operations", func(t *testing.T) { + t.Run("viewer cannot create folders", func(t *testing.T) { + // Create a folder by POSTing to a directory path + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + url := fmt.Sprintf("http://viewer:viewer@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/test-folder/", addr, repo) + req, err := http.NewRequest(http.MethodPost, url, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + + require.Equal(t, http.StatusForbidden, resp.StatusCode, "viewer should not be able to create folders") + }) + + t.Run("editor can create folders", func(t *testing.T) { + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + url := fmt.Sprintf("http://editor:editor@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/editor-folder/", addr, repo) + req, err := http.NewRequest(http.MethodPost, url, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode, "editor should be able to create folders") + + // Clean up - delete folder + deleteURL := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/editor-folder/", addr, repo) + deleteReq, err := http.NewRequest(http.MethodDelete, deleteURL, nil) + require.NoError(t, err) + deleteResp, err := http.DefaultClient.Do(deleteReq) + require.NoError(t, err) + // nolint:errcheck + defer deleteResp.Body.Close() + }) + + t.Run("admin can create folders", func(t *testing.T) { + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + url := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/admin-folder/", addr, repo) + req, err := http.NewRequest(http.MethodPost, url, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode, "admin should be able to create folders") + + // Clean up - delete folder + deleteURL := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/admin-folder/", addr, repo) + deleteReq, err := http.NewRequest(http.MethodDelete, deleteURL, nil) + require.NoError(t, err) + deleteResp, err := http.DefaultClient.Do(deleteReq) + require.NoError(t, err) + // nolint:errcheck + defer deleteResp.Body.Close() + }) + }) +} diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index f4687bdb01b..791ac4b8a20 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -53,6 +53,7 @@ type provisioningTestHelper struct { ProvisioningPath string Repositories *apis.K8sResourceClient + Connections *apis.K8sResourceClient Jobs *apis.K8sResourceClient Folders *apis.K8sResourceClient DashboardsV0 *apis.K8sResourceClient @@ -680,15 +681,23 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper EnableFeatureToggles: []string{ featuremgmt.FlagProvisioning, }, + // Provisioning requires resources to be fully migrated to unified storage. + // Mode5 ensures reads/writes go to unified storage, and EnableMigration + // enables the data migration at startup to migrate legacy data. UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { - DualWriterMode: grafanarest.Mode5, + DualWriterMode: grafanarest.Mode5, + EnableMigration: true, }, "folders.folder.grafana.app": { - DualWriterMode: grafanarest.Mode5, + DualWriterMode: grafanarest.Mode5, + EnableMigration: true, }, }, PermittedProvisioningPaths: ".|" + provisioningPath, + // Allow both folder and instance sync targets for tests + // (instance is needed for export jobs, folder for most operations) + ProvisioningAllowedTargets: []string{"folder", "instance"}, } for _, o := range options { o(&opts) @@ -703,6 +712,11 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper Namespace: "default", // actually org1 GVR: provisioning.RepositoryResourceInfo.GroupVersionResource(), }) + connections := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: provisioning.ConnectionResourceInfo.GroupVersionResource(), + }) jobs := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: "default", // actually org1 @@ -763,6 +777,7 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper K8sTestHelper: helper, Repositories: repositories, + Connections: connections, AdminREST: adminClient, EditorREST: editorClient, ViewerREST: viewerClient, @@ -957,3 +972,49 @@ func (h *provisioningTestHelper) CleanupAllRepos(t *testing.T) { assert.Equal(collect, 0, len(list.Items), "repositories should be cleaned up") }, waitTimeoutDefault, waitIntervalDefault, "repositories should be cleaned up between subtests") } + +func postHelper(t *testing.T, helper apis.K8sTestHelper, path string, body interface{}, user apis.User) (map[string]interface{}, int, error) { + return requestHelper(t, helper, http.MethodPost, path, body, user) +} + +func patchHelper(t *testing.T, helper apis.K8sTestHelper, path string, body interface{}, user apis.User) (map[string]interface{}, int, error) { + return requestHelper(t, helper, http.MethodPatch, path, body, user) +} + +func requestHelper( + t *testing.T, + helper apis.K8sTestHelper, + method string, + path string, + body interface{}, + user apis.User, +) (map[string]interface{}, int, error) { + bodyJSON, err := json.Marshal(body) + require.NoError(t, err) + + resp := apis.DoRequest(&helper, apis.RequestParams{ + User: user, + Method: method, + Path: path, + Body: bodyJSON, + ContentType: "application/json", + }, &struct{}{}) + + if resp.Response.StatusCode != http.StatusOK { + res := map[string]interface{}{} + err := json.Unmarshal(resp.Body, &res) + if err != nil { + return nil, 0, fmt.Errorf("failed to unmarshal response JSON: %v", err) + } + + return res, resp.Response.StatusCode, fmt.Errorf("failure when making request: %s", resp.Response.Status) + } + + var result map[string]interface{} + err = json.Unmarshal(resp.Body, &result) + if err != nil { + return nil, 0, fmt.Errorf("failed to unmarshal response JSON: %v", err) + } + + return result, resp.Response.StatusCode, nil +} diff --git a/pkg/tests/apis/provisioning/historicjobs_auth_test.go b/pkg/tests/apis/provisioning/historicjobs_auth_test.go new file mode 100644 index 00000000000..fc11f8d00b3 --- /dev/null +++ b/pkg/tests/apis/provisioning/historicjobs_auth_test.go @@ -0,0 +1,95 @@ +package provisioning + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_HistoricJobsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "historicjobs-auth-test" + testRepo := TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, // No files needed for this test + ExpectedDashboards: 0, + ExpectedFolders: 1, // Repository creates a folder + } + helper.CreateRepo(t, testRepo) + + // Trigger a job to create a historic job entry + jobSpec := provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{}, + } + body := asJSON(jobSpec) + + // Create a job as admin + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + require.NoError(t, result.Error(), "should be able to create job") + require.Equal(t, http.StatusAccepted, statusCode) + + // Wait for job to complete and become historic + helper.AwaitJobs(t, repo) + historicJob := helper.AwaitLatestHistoricJob(t, repo) + require.NotNil(t, historicJob, "should have a historic job") + + historicJobName := historicJob.GetName() + + t.Run("admin can GET historic job", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET historic job") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET historic job", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET historic job") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET historic job", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET historic job") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} diff --git a/pkg/tests/apis/provisioning/job_validation_test.go b/pkg/tests/apis/provisioning/job_validation_test.go index c54ad58500c..4c1947693f1 100644 --- a/pkg/tests/apis/provisioning/job_validation_test.go +++ b/pkg/tests/apis/provisioning/job_validation_test.go @@ -24,10 +24,10 @@ func TestIntegrationProvisioning_JobValidation(t *testing.T) { const repo = "job-validation-test-repo" testRepo := TestRepo{ Name: repo, - Target: "instance", + Target: "folder", Copies: map[string]string{}, ExpectedDashboards: 0, - ExpectedFolders: 0, + ExpectedFolders: 1, // folder sync creates a folder } helper.CreateRepo(t, testRepo) diff --git a/pkg/tests/apis/provisioning/librarypanels_test.go b/pkg/tests/apis/provisioning/librarypanels_test.go new file mode 100644 index 00000000000..47f87e7fb86 --- /dev/null +++ b/pkg/tests/apis/provisioning/librarypanels_test.go @@ -0,0 +1,175 @@ +package provisioning + +import ( + "fmt" + "net/http" + "testing" + "time" + + foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/util/testutil" + "github.com/stretchr/testify/assert" + "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/types" +) + +// We currently block the creation of library panels in provisioned folders. +func TestIntegrationLibraryPanels_ProvisionedFolders(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + helper.CreateRepo(t, TestRepo{ + Name: "test-repo", + Target: "folder", + ExpectedFolders: 1, + }) + + t.Run("should fail to create library element in provisioned folder", func(t *testing.T) { + folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, folders.Items, 1) + + managedFolderName := folders.Items[0].GetName() + libraryElement := map[string]interface{}{ + "kind": 1, + "name": "Library Panel", + "folderUid": managedFolderName, + "model": map[string]interface{}{ + "type": "text", + "title": "Library Panel", + }, + } + libraryElementURL := "/api/library-elements" + libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin) + require.Error(t, err) + require.Equal(t, http.StatusConflict, code) + require.NotNil(t, libraryElementData) + require.Equal(t, "resource type not supported in repository-managed folders", libraryElementData["message"]) + }) + + t.Run("should fail to patch library element, moving it in a provisioned folder", func(t *testing.T) { + // Getting managed folder + folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, folders.Items, 1) + managedFolderName := folders.Items[0].GetName() + + unmanagedFolder := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": foldersV1.FolderResourceInfo.GroupVersion().String(), + "kind": foldersV1.FolderResourceInfo.GroupVersionKind().Kind, + "metadata": map[string]interface{}{ + "generateName": "test-folder-", + }, + "spec": map[string]interface{}{ + "title": "Library Panel", + }, + }, + } + createdFolder, err := helper.Folders.Resource.Create(t.Context(), unmanagedFolder, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, createdFolder) + + libraryElement := map[string]interface{}{ + "kind": 1, + "name": "Moved Library Panel", + "folderUid": createdFolder.GetName(), + "model": map[string]interface{}{ + "type": "text", + "title": "Moved Library Panel", + }, + } + libraryElementURL := "/api/library-elements" + libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin) + require.NoError(t, err) + require.Equal(t, http.StatusOK, code) + require.NotNil(t, libraryElementData) + + res := libraryElementData["result"].(map[string]interface{}) + helper.SetPermissions(helper.Org1.Admin, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{"library.panels:write"}, + Resource: "library.panels", + ResourceAttribute: "uid", + ResourceID: "*", + }, + }) + + // Patching libraryElement - changing folder to a managed one + updatedLibraryElement := map[string]interface{}{ + "kind": 1, + "version": res["version"], + "folderUid": managedFolderName, + } + patchLibraryElementURL := fmt.Sprintf("/api/library-elements/%f", +res["id"].(float64)) + newLibraryElement, code, err := patchHelper(t, *helper.K8sTestHelper, patchLibraryElementURL, updatedLibraryElement, helper.Org1.Admin) + require.Error(t, err) + require.Equal(t, http.StatusConflict, code) + require.NotNil(t, newLibraryElement) + require.Equal(t, "resource type not supported in repository-managed folders", newLibraryElement["message"]) + }) +} + +func TestIntegrationLibraryPanels_UnprovisionedFolders(t *testing.T) { + const repo = "test-repo" + helper := runGrafana(t) + helper.CreateRepo(t, TestRepo{ + Name: repo, + Target: "folder", + ExpectedFolders: 1, + }) + + t.Run("should create library element when folder is released", func(t *testing.T) { + folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, folders.Items, 1) + managedFolderName := folders.Items[0].GetName() + require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerKind, "folder should be managed") + require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerIdentity, "folder should be managed") + + _, err = helper.Repositories.Resource.Patch(t.Context(), repo, types.JSONPatchType, []byte(`[ + { + "op": "replace", + "path": "/metadata/finalizers", + "value": ["cleanup", "release-orphan-resources"] + } + ]`), metav1.PatchOptions{}) + require.NoError(t, err, "should successfully patch finalizers") + + require.NoError(t, helper.Repositories.Resource.Delete(t.Context(), repo, metav1.DeleteOptions{})) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := helper.Repositories.Resource.Get(t.Context(), repo, metav1.GetOptions{}) + assert.True(collect, apierrors.IsNotFound(err), "repository should be deleted") + }, time.Second*10, time.Millisecond*50, "repository should be deleted") + require.EventuallyWithT(t, func(collect *assert.CollectT) { + foundFolders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err, "can list values") + for _, v := range foundFolders.Items { + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourcePath) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum) + } + }, time.Second*20, time.Millisecond*10, "Expected folders to be released") + + libraryElement := map[string]interface{}{ + "kind": 1, + "name": "Library Panel", + "folderUid": managedFolderName, + "model": map[string]interface{}{ + "type": "text", + "title": "Library Panel", + }, + } + libraryElementURL := "/api/library-elements" + libraryElementData, code, err := postHelper(t, *helper.K8sTestHelper, libraryElementURL, libraryElement, helper.Org1.Admin) + require.NoError(t, err) + require.Equal(t, http.StatusOK, code) + require.NotNil(t, libraryElementData) + }) +} diff --git a/pkg/tests/apis/provisioning/movejob_test.go b/pkg/tests/apis/provisioning/movejob_test.go index 85d227996fd..9cd298711a9 100644 --- a/pkg/tests/apis/provisioning/movejob_test.go +++ b/pkg/tests/apis/provisioning/movejob_test.go @@ -24,14 +24,15 @@ func TestIntegrationProvisioning_MoveJob(t *testing.T) { ctx := context.Background() const repo = "move-test-repo" testRepo := TestRepo{ - Name: repo, + Name: repo, + Target: "folder", Copies: map[string]string{ "testdata/all-panels.json": "dashboard1.json", "testdata/text-options.json": "dashboard2.json", "testdata/timeline-demo.json": "folder/dashboard3.json", }, ExpectedDashboards: 3, - ExpectedFolders: 1, + ExpectedFolders: 2, // folder sync creates a folder for the repo + one nested folder } helper.CreateRepo(t, testRepo) @@ -236,6 +237,7 @@ func TestIntegrationProvisioning_MoveJob(t *testing.T) { const refRepo = "move-ref-test-repo" helper.CreateRepo(t, TestRepo{ Name: refRepo, + Target: "folder", SkipResourceAssertions: true, // HACK: I am not sure why sometimes it's 6 or 3 dashbaords. }) diff --git a/pkg/tests/apis/provisioning/repository_subresources_auth_test.go b/pkg/tests/apis/provisioning/repository_subresources_auth_test.go new file mode 100644 index 00000000000..54a2f173b51 --- /dev/null +++ b/pkg/tests/apis/provisioning/repository_subresources_auth_test.go @@ -0,0 +1,236 @@ +package provisioning + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_RepositorySubresourcesAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "subresources-auth-test" + testRepo := TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, // No files needed for this test + ExpectedDashboards: 0, + ExpectedFolders: 1, // Repository creates a folder + } + helper.CreateRepo(t, testRepo) + + t.Run("test subresource", func(t *testing.T) { + newRepoConfig := map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "spec": map[string]any{ + "title": "Test Configuration", + "type": "local", + "local": map[string]any{ + "path": helper.ProvisioningPath, + }, + "workflows": []string{"write"}, + "sync": map[string]any{ + "enabled": true, + "target": "folder", + "intervalSeconds": 10, + }, + }, + } + configBytes, err := json.Marshal(newRepoConfig) + require.NoError(t, err) + + t.Run("admin can POST test", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to POST test") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot POST test", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to POST test") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot POST test", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to POST test") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("resources subresource", func(t *testing.T) { + t.Run("admin can GET resources", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET resources") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET resources", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET resources") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET resources", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET resources") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("history subresource", func(t *testing.T) { + t.Run("admin can GET history (or BadRequest if not supported)", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + // Admin should pass authorization - may get BadRequest if repo doesn't support history + // but should NOT get Forbidden (which would indicate authorization failure) + if result.Error() != nil { + require.False(t, apierrors.IsForbidden(result.Error()), "admin should not get Forbidden error") + // Local repos don't support history, so BadRequest is expected + require.True(t, apierrors.IsBadRequest(result.Error()) || statusCode == http.StatusBadRequest, + "should get BadRequest if history not supported, not Forbidden") + } else { + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK if history is supported") + } + }) + + t.Run("editor cannot GET history", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET history") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET history", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET history") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("status subresource", func(t *testing.T) { + t.Run("admin can GET status", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET status") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET status", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET status", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) +} diff --git a/pkg/tests/apis/provisioning/repository_test.go b/pkg/tests/apis/provisioning/repository_test.go index d7850c52d0a..f2447e71d23 100644 --- a/pkg/tests/apis/provisioning/repository_test.go +++ b/pkg/tests/apis/provisioning/repository_test.go @@ -578,7 +578,13 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) { const targetPath = "all-panels.json" // Set up the repository. - helper.CreateRepo(t, TestRepo{Name: repo}) + helper.CreateRepo(t, TestRepo{ + Name: repo, + Target: "folder", + ExpectedDashboards: 0, + ExpectedFolders: 1, // folder sync creates a folder for the repo + SkipResourceAssertions: false, + }) // Write a file -- this will create it *both* in the local file system, and in grafana t.Run("write all panels", func(t *testing.T) { @@ -744,10 +750,10 @@ func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository(t *testing.T // Set up the repository and the file to import. testRepo := TestRepo{ Name: repo, - Target: "instance", + Target: "folder", Copies: map[string]string{"testdata/all-panels.json": "all-panels.json"}, ExpectedDashboards: 1, - ExpectedFolders: 0, + ExpectedFolders: 1, // folder sync creates a folder } // We create the repository helper.CreateRepo(t, testRepo) @@ -786,7 +792,7 @@ func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository(t *testing.T v, _, _ := unstructured.NestedString(obj.Object, "metadata", "annotations", utils.AnnoKeyUpdatedBy) require.Equal(t, "access-policy:provisioning", v) - // Should not be able to directly delete the managed resource + // Should be able to directly delete the managed resource err = helper.DashboardsV1.Resource.Delete(ctx, allPanels, metav1.DeleteOptions{}) require.NoError(t, err, "user can delete") @@ -867,3 +873,149 @@ func TestIntegrationProvisioning_DeleteRepositoryAndReleaseResources(t *testing. } }, time.Second*20, time.Millisecond*10, "Expected folders to be released") } + +func TestIntegrationProvisioning_JobPermissions(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "job-permissions-test" + testRepo := TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, // No files needed for this test + ExpectedDashboards: 0, + ExpectedFolders: 1, // Repository creates a folder + } + helper.CreateRepo(t, testRepo) + + jobSpec := provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{}, + } + body := asJSON(jobSpec) + + t.Run("editor can POST jobs", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to POST jobs") + require.Equal(t, http.StatusAccepted, statusCode, "should return 202 Accepted") + + // Verify the job was created + obj, err := result.Get() + require.NoError(t, err, "should get job object") + unstruct, ok := obj.(*unstructured.Unstructured) + require.True(t, ok, "expecting unstructured object") + require.NotEmpty(t, unstruct.GetName(), "job should have a name") + }) + + t.Run("viewer cannot POST jobs", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to POST jobs") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("admin can POST jobs", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + // Job might already exist from previous test, which is acceptable + if apierrors.IsAlreadyExists(result.Error()) { + // Wait for the existing job to complete + helper.AwaitJobs(t, repo) + return + } + + require.NoError(t, result.Error(), "admin should be able to POST jobs") + require.Equal(t, http.StatusAccepted, statusCode, "should return 202 Accepted") + }) +} + +func TestIntegrationProvisioning_RefsPermissions(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "refs-permissions-test" + testRepo := TestRepo{ + Name: repo, + Template: "testdata/github-readonly.json.tmpl", + Target: "folder", + ExpectedDashboards: 3, + ExpectedFolders: 3, // Repository creates folders + } + helper.CreateRepo(t, testRepo) + + t.Run("editor can GET refs", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("refs"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to GET refs") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + + // Verify we can parse the refs and it contains at least main branch + refs := &provisioning.RefList{} + err := result.Into(refs) + require.NoError(t, err, "should parse refs response") + require.NotEmpty(t, refs.Items, "should have at least one ref") + }) + + t.Run("viewer cannot GET refs", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("refs"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET refs") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("admin can GET refs", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("refs"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET refs") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) +} diff --git a/pkg/tests/apis/provisioning/settings_stats_auth_test.go b/pkg/tests/apis/provisioning/settings_stats_auth_test.go new file mode 100644 index 00000000000..d438066235a --- /dev/null +++ b/pkg/tests/apis/provisioning/settings_stats_auth_test.go @@ -0,0 +1,104 @@ +package provisioning + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_SettingsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + t.Run("viewer can GET settings", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "viewer should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor can GET settings", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("admin can GET settings", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) +} + +func TestIntegrationProvisioning_StatsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + // Create a repository to ensure stats endpoint has data + const repo = "stats-auth-test" + helper.CreateRepo(t, TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, + ExpectedDashboards: 0, + ExpectedFolders: 1, + }) + + t.Run("admin can GET stats", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET stats") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET stats", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET stats") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET stats", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET stats") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} diff --git a/pkg/tests/apis/provisioning/stats_test.go b/pkg/tests/apis/provisioning/stats_test.go index eae3cbf7531..25ecd5c8f7c 100644 --- a/pkg/tests/apis/provisioning/stats_test.go +++ b/pkg/tests/apis/provisioning/stats_test.go @@ -21,13 +21,14 @@ func TestIntegrationProvisioning_Stats(t *testing.T) { const repo = "stats-test-repo1" testRepo := TestRepo{ - Name: repo, + Name: repo, + Target: "folder", Copies: map[string]string{ "testdata/all-panels.json": "dashboard1.json", "testdata/text-options.json": "folder/dashboard2.json", }, ExpectedDashboards: 2, - ExpectedFolders: 1, + ExpectedFolders: 2, // folder sync creates a folder for the repo + one nested folder } helper.CreateRepo(t, testRepo) @@ -94,7 +95,7 @@ func TestIntegrationProvisioning_Stats(t *testing.T) { require.Equal(t, int64(2), count, "repo should manage 2 dashboards") } else if group == "folder.grafana.app" && resource == "folders" { count, _, _ := unstructured.NestedInt64(stat, "count") - require.Equal(t, int64(1), count, "repo should manage 1 folder") + require.Equal(t, int64(2), count, "repo should manage 2 folders (repo folder + nested folder)") } } } diff --git a/pkg/tests/apis/zanzana_reconcile.go b/pkg/tests/apis/zanzana_reconcile.go new file mode 100644 index 00000000000..f8a5673fed7 --- /dev/null +++ b/pkg/tests/apis/zanzana_reconcile.go @@ -0,0 +1,87 @@ +package apis + +import ( + "bytes" + "context" + "net/http" + "testing" + "time" + + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +const zanzanaReconcileLastSuccessMetric = "grafana_zanzana_reconcile_last_success_timestamp_seconds" + +// AwaitZanzanaReconcileNext waits for the next Zanzana reconciliation cycle to complete. +// It is a no-op unless the `zanzana` feature toggle is enabled for the running test env. +func AwaitZanzanaReconcileNext(t *testing.T, helper *K8sTestHelper) { + t.Helper() + + enabled := false + if helper != nil { + enabled = helper.GetEnv().FeatureToggles.GetEnabled(context.Background())[featuremgmt.FlagZanzana] + } + if helper == nil || !enabled { + return + } + + prev, ok := getZanzanaReconcileLastSuccessTimestampSeconds(t, helper) + if !ok { + prev = 0 + } + + require.EventuallyWithT(t, func(c *assert.CollectT) { + ts, ok := getZanzanaReconcileLastSuccessTimestampSeconds(t, helper) + assert.True(c, ok, "expected to find %s in /metrics", zanzanaReconcileLastSuccessMetric) + if !ok { + return + } + assert.Greater(c, ts, prev, "expected %s (%v) > %v", zanzanaReconcileLastSuccessMetric, ts, prev) + }, 30*time.Second, 50*time.Millisecond) +} + +func getZanzanaReconcileLastSuccessTimestampSeconds(t *testing.T, helper *K8sTestHelper) (float64, bool) { + t.Helper() + + rsp := DoRequest(helper, RequestParams{ + User: helper.Org1.Admin, + Path: "/metrics", + Accept: "text/plain", + }, &struct{}{}) + if rsp.Response == nil || rsp.Response.StatusCode != http.StatusOK { + return 0, false + } + + parser := expfmt.NewTextParser(model.UTF8Validation) + metrics, err := parser.TextToMetricFamilies(bytes.NewReader(rsp.Body)) + if err != nil { + return 0, false + } + + metric := metrics[zanzanaReconcileLastSuccessMetric] + if metric == nil || len(metric.Metric) == 0 { + return 0, false + } + + m := metric.Metric[0] + switch metric.GetType() { + case dto.MetricType_GAUGE: + if m.Gauge == nil { + return 0, false + } + return m.Gauge.GetValue(), true + case dto.MetricType_UNTYPED: + if m.Untyped == nil { + return 0, false + } + return m.Untyped.GetValue(), true + default: + return 0, false + } +} diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index e9795f945ab..13507b7987a 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -362,6 +362,13 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = rbacSect.NewKey("permission_cache", "false") require.NoError(t, err) + if opts.DisableAuthZClientCache { + authzSect, err := cfg.NewSection("authorization") + require.NoError(t, err) + _, err = authzSect.NewKey("cache_ttl", "0") + require.NoError(t, err) + } + analyticsSect, err := cfg.NewSection("analytics") require.NoError(t, err) _, err = analyticsSect.NewKey("intercom_secret", "intercom_secret_at_config") @@ -547,6 +554,8 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) _, err = section.NewKey("dualWriterMode", fmt.Sprintf("%d", v.DualWriterMode)) require.NoError(t, err) + _, err = section.NewKey("enableMigration", fmt.Sprintf("%t", v.EnableMigration)) + require.NoError(t, err) } } if opts.UnifiedStorageEnableSearch { @@ -571,6 +580,12 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = pathsSect.NewKey("permitted_provisioning_paths", opts.PermittedProvisioningPaths) require.NoError(t, err) } + if len(opts.ProvisioningAllowedTargets) > 0 { + provisioningSect, err := getOrCreateSection("provisioning") + require.NoError(t, err) + _, err = provisioningSect.NewKey("allowed_targets", strings.Join(opts.ProvisioningAllowedTargets, "|")) + require.NoError(t, err) + } if opts.EnableSCIM { scimSection, err := getOrCreateSection("auth.scim") require.NoError(t, err) @@ -594,6 +609,20 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) } + if opts.ZanzanaReconciliationInterval != 0 { + rbacSect, err := cfg.NewSection("rbac") + require.NoError(t, err) + _, err = rbacSect.NewKey("zanzana_reconciliation_interval", opts.ZanzanaReconciliationInterval.String()) + require.NoError(t, err) + } + + if opts.DisableZanzanaCache { + rbacSect, err := cfg.NewSection("rbac") + require.NoError(t, err) + _, err = rbacSect.NewKey("disable_zanzana_cache", "true") + require.NoError(t, err) + } + dashboardsSection, err := getOrCreateSection("dashboards") require.NoError(t, err) _, err = dashboardsSection.NewKey("min_refresh_interval", "10s") @@ -660,6 +689,7 @@ type GrafanaOpts struct { UnifiedStorageEnableSearch bool UnifiedStorageMaxPageSizeBytes int PermittedProvisioningPaths string + ProvisioningAllowedTargets []string GrafanaComSSOAPIToken string LicensePath string EnableRecordingRules bool @@ -670,6 +700,9 @@ type GrafanaOpts struct { DisableDataMigrations bool SecretsManagerEnableDBMigrations bool OpenFeatureAPIEnabled bool + DisableAuthZClientCache bool + ZanzanaReconciliationInterval time.Duration + DisableZanzanaCache bool // Allow creating grafana dir beforehand Dir string diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index c8648f0bdeb..de8dda2b61c 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -20,10 +20,18 @@ type SearchRequest struct { Aggs AggArray CustomProps map[string]interface{} TimeRange backend.TimeRange + // RawBody contains the raw Elasticsearch Query DSL JSON for raw DSL queries + // When set, this takes precedence over all other fields during marshaling + RawBody map[string]interface{} } // MarshalJSON returns the JSON encoding of the request. func (r *SearchRequest) MarshalJSON() ([]byte, error) { + // If RawBody is set, use it directly for raw DSL queries + if len(r.RawBody) > 0 { + return json.Marshal(r.RawBody) + } + root := make(map[string]interface{}) root["size"] = r.Size diff --git a/pkg/tsdb/elasticsearch/client/request_encoder.go b/pkg/tsdb/elasticsearch/client/request_encoder.go index ae22c8e2694..0c6e2314d99 100644 --- a/pkg/tsdb/elasticsearch/client/request_encoder.go +++ b/pkg/tsdb/elasticsearch/client/request_encoder.go @@ -3,6 +3,7 @@ package es import ( "bytes" "encoding/json" + "fmt" "strconv" "strings" "time" @@ -25,6 +26,9 @@ func newRequestEncoder(logger log.Logger) *requestEncoder { // encodeBatchRequests encodes multiple requests into NDJSON format func (e *requestEncoder) encodeBatchRequests(requests []*multiRequest) ([]byte, error) { start := time.Now() + defer func() { + e.logger.Debug("Completed encoding of batch requests to json", "duration", time.Since(start)) + }() payload := bytes.Buffer{} for _, r := range requests { @@ -34,20 +38,25 @@ func (e *requestEncoder) encodeBatchRequests(requests []*multiRequest) ([]byte, } payload.WriteString(string(reqHeader) + "\n") - reqBody, err := json.Marshal(r.body) - if err != nil { - return nil, err + body := "" + switch r.body.(type) { + case *SearchRequest: + reqBody, err := json.Marshal(r.body) + if err != nil { + return nil, err + } + body = string(reqBody) + case string: + body = r.body.(string) + default: + return nil, fmt.Errorf("unknown request type: %T", r.body) } - body := string(reqBody) body = strings.ReplaceAll(body, "$__interval_ms", strconv.FormatInt(r.interval.Milliseconds(), 10)) body = strings.ReplaceAll(body, "$__interval", r.interval.String()) payload.WriteString(body + "\n") } - elapsed := time.Since(start) - e.logger.Debug("Completed encoding of batch requests to json", "duration", elapsed) - return payload.Bytes(), nil } diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index f898517ab07..8f47d240a28 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -30,6 +30,8 @@ type SearchRequestBuilder struct { aggBuilders []AggBuilder customProps map[string]any timeRange backend.TimeRange + // rawBody contains the raw Elasticsearch Query DSL JSON for raw DSL queries + rawBody map[string]any } // NewSearchRequestBuilder create a new search request builder @@ -53,6 +55,12 @@ func (b *SearchRequestBuilder) Build() (*SearchRequest, error) { Size: b.size, Sort: b.sort, CustomProps: b.customProps, + RawBody: b.rawBody, + } + + // If RawBody is set, skip building query and aggs as they're in the raw body + if len(b.rawBody) > 0 { + return &sr, nil } if b.queryBuilder != nil { @@ -141,6 +149,19 @@ func (b *SearchRequestBuilder) AddSearchAfter(value any) *SearchRequestBuilder { return b } +// AddCustomProp adds a custom property to the search request +func (b *SearchRequestBuilder) AddCustomProp(key string, value any) *SearchRequestBuilder { + b.customProps[key] = value + return b +} + +// SetRawBody sets the raw Elasticsearch Query DSL body directly +// This bypasses all builder logic and sends the query as-is to Elasticsearch +func (b *SearchRequestBuilder) SetRawBody(rawBody map[string]any) *SearchRequestBuilder { + b.rawBody = rawBody + return b +} + // Query creates and return a query builder func (b *SearchRequestBuilder) Query() *QueryBuilder { if b.queryBuilder == nil { diff --git a/pkg/tsdb/elasticsearch/data_query.go b/pkg/tsdb/elasticsearch/data_query.go index e883b3d769c..949b93fd148 100644 --- a/pkg/tsdb/elasticsearch/data_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -20,11 +20,12 @@ const ( ) type elasticsearchDataQuery struct { - client es.Client - dataQueries []backend.DataQuery - logger log.Logger - ctx context.Context - keepLabelsInResponse bool + client es.Client + dataQueries []backend.DataQuery + logger log.Logger + ctx context.Context + keepLabelsInResponse bool + aggregationParserDSLRawQuery AggregationParser } var newElasticsearchDataQuery = func(ctx context.Context, client es.Client, req *backend.QueryDataRequest, logger log.Logger) *elasticsearchDataQuery { @@ -39,6 +40,8 @@ var newElasticsearchDataQuery = func(ctx context.Context, client es.Client, req // To maintain backward compatibility, it is necessary to keep labels in responses for alerting and expressions queries. // Historically, these labels have been used in alerting rules and transformations. keepLabelsInResponse: fromAlert || fromExpression, + + aggregationParserDSLRawQuery: NewAggregationParser(), } } diff --git a/pkg/tsdb/elasticsearch/data_query_processor.go b/pkg/tsdb/elasticsearch/data_query_processor.go index 5f6ea448ddd..288d6ce30de 100644 --- a/pkg/tsdb/elasticsearch/data_query_processor.go +++ b/pkg/tsdb/elasticsearch/data_query_processor.go @@ -1,6 +1,7 @@ package elasticsearch import ( + "encoding/json" "fmt" "strconv" @@ -23,6 +24,17 @@ func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchReques filters.AddDateRangeFilter(defaultTimeField, to, from, es.DateFormatEpochMS) filters.AddQueryStringFilter(q.RawQuery, true) + if q.EditorType != nil && *q.EditorType == "code" { + cfg := backend.GrafanaConfigFromContext(e.ctx) + if !cfg.FeatureToggles().IsEnabled("elasticsearchRawDSLQuery") { + return backend.DownstreamError(fmt.Errorf("raw DSL query feature is disabled. Enable the elasticsearchRawDSLQuery feature toggle to use this query type")) + } + + if err := e.processRawDSLQuery(q, b); err != nil { + return err + } + } + if isLogsQuery(q) { processLogsQuery(q, b, from, to, defaultTimeField) } else if isDocumentQuery(q) { @@ -184,6 +196,46 @@ func processTimeSeriesQuery(q *Query, b *es.SearchRequestBuilder, from, to int64 } } +func (e *elasticsearchDataQuery) processRawDSLQuery(q *Query, b *es.SearchRequestBuilder) error { + if q.RawDSLQuery == "" { + return backend.DownstreamError(fmt.Errorf("raw DSL query is empty")) + } + + // Parse the raw DSL query JSON + var queryBody map[string]any + if err := json.Unmarshal([]byte(q.RawDSLQuery), &queryBody); err != nil { + return backend.DownstreamError(fmt.Errorf("invalid raw DSL query JSON: %w", err)) + } + + if len(q.Metrics) > 0 { + firstMetricType := q.Metrics[0].Type + if firstMetricType != logsType && firstMetricType != rawDataType && firstMetricType != rawDocumentType { + bucketAggs, metricAggs, err := e.aggregationParserDSLRawQuery.Parse(q.RawDSLQuery) + if err != nil { + return backend.DownstreamError(fmt.Errorf("failed to parse aggregations: %w", err)) + } + + // If there is no metric agg in the query, it is a count agg + if len(metricAggs) == 0 { + metricAggs = append(metricAggs, &MetricAgg{Type: "count"}) + } + + q.BucketAggs = bucketAggs + q.Metrics = metricAggs + + if queryPart, ok := queryBody["query"].(map[string]any); ok { + queryJSON, _ := json.Marshal(queryPart) + q.RawQuery = string(queryJSON) + } + return nil + } + } + + // For non-time-series queries (logs, raw data), pass through the raw body directly + b.SetRawBody(queryBody) + return nil +} + // getPipelineAggField returns the pipeline aggregation field func getPipelineAggField(m *MetricAgg) string { // In frontend we are using Field as pipelineAggField diff --git a/pkg/tsdb/elasticsearch/data_query_test.go b/pkg/tsdb/elasticsearch/data_query_test.go index 887b8ba661d..e2531766eaf 100644 --- a/pkg/tsdb/elasticsearch/data_query_test.go +++ b/pkg/tsdb/elasticsearch/data_query_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1887,6 +1888,11 @@ func newDataQuery(body string) (backend.QueryDataRequest, error) { } func executeElasticsearchDataQuery(c es.Client, body string, from, to time.Time) ( + *backend.QueryDataResponse, error) { + return executeElasticsearchDataQueryWithContext(c, body, from, to, context.Background()) +} + +func executeElasticsearchDataQueryWithContext(c es.Client, body string, from, to time.Time, ctx context.Context) ( *backend.QueryDataResponse, error) { timeRange := backend.TimeRange{ From: from, @@ -1901,6 +1907,98 @@ func executeElasticsearchDataQuery(c es.Client, body string, from, to time.Time) }, }, } - query := newElasticsearchDataQuery(context.Background(), c, &dataRequest, log.New()) + query := newElasticsearchDataQuery(ctx, c, &dataRequest, log.New()) return query.execute() } + +func TestRawDSLQuery(t *testing.T) { + from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) + to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) + + // Create context with raw DSL query feature toggle enabled + cfg := backend.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "elasticsearchRawDSLQuery", + }) + ctx := backend.WithGrafanaConfig(context.Background(), cfg) + + t.Run("With raw DSL query", func(t *testing.T) { + t.Run("Basic raw DSL query with aggregations", func(t *testing.T) { + c := newFakeClient() + _, err := executeElasticsearchDataQueryWithContext(c, `{ + "editorType": "code", + "rawDSLQuery": "{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"@timestamp\":{\"gte\":1526405400000,\"lte\":1526405700000,\"format\":\"epoch_millis\"}}}]}},\"aggs\":{\"date_histogram\":{\"date_histogram\":{\"field\":\"@timestamp\",\"interval\":\"1m\"}}},\"size\":0}" + }`, from, to, ctx) + require.NoError(t, err) + require.Len(t, c.multisearchRequests, 1) + require.Len(t, c.multisearchRequests[0].Requests, 1) + sr := c.multisearchRequests[0].Requests[0] + + // Verify RawBody contains the entire DSL query + require.NotNil(t, sr.RawBody) + require.Contains(t, sr.RawBody, "query") + require.Contains(t, sr.RawBody, "aggs") + + // Verify size from raw body + size, ok := sr.RawBody["size"].(float64) + require.True(t, ok) + require.Equal(t, float64(0), size) + }) + + t.Run("Raw DSL query with query_string", func(t *testing.T) { + c := newFakeClient() + _, err := executeElasticsearchDataQueryWithContext(c, `{ + "editorType": "code", + "rawDSLQuery": "{\"query\":{\"query_string\":{\"query\":\"status:200\",\"analyze_wildcard\":true}},\"size\":100}" + }`, from, to, ctx) + require.NoError(t, err) + require.Len(t, c.multisearchRequests, 1) + sr := c.multisearchRequests[0].Requests[0] + + // Verify RawBody contains the entire DSL query + require.NotNil(t, sr.RawBody) + require.Contains(t, sr.RawBody, "query") + + // Verify size from raw body + size, ok := sr.RawBody["size"].(float64) + require.True(t, ok) + require.Equal(t, float64(100), size) + + // Verify query object exists in raw body + query, ok := sr.RawBody["query"].(map[string]any) + require.True(t, ok) + require.Contains(t, query, "query_string") + }) + + t.Run("Raw DSL query with sort", func(t *testing.T) { + c := newFakeClient() + _, err := executeElasticsearchDataQueryWithContext(c, `{ + "editorType": "code", + "rawDSLQuery": "{\"query\":{\"match_all\":{}},\"sort\":[{\"@timestamp\":{\"order\":\"desc\"}}],\"size\":50}" + }`, from, to, ctx) + require.NoError(t, err) + require.Len(t, c.multisearchRequests, 1) + sr := c.multisearchRequests[0].Requests[0] + + // Verify RawBody contains the entire DSL query + require.NotNil(t, sr.RawBody) + require.Contains(t, sr.RawBody, "query") + require.Contains(t, sr.RawBody, "sort") + + // Verify sort in raw body + sort, ok := sr.RawBody["sort"].([]any) + require.True(t, ok) + require.NotEmpty(t, sort) + }) + + t.Run("Invalid JSON in raw DSL query returns error", func(t *testing.T) { + c := newFakeClient() + response, err := executeElasticsearchDataQueryWithContext(c, `{ + "editorType": "code", + "rawDSLQuery": "{ invalid json }" + }`, from, to, ctx) + require.NoError(t, err) + require.NotNil(t, response.Responses["A"].Error) + require.Contains(t, response.Responses["A"].Error.Error(), "invalid raw DSL query JSON") + }) + }) +} diff --git a/pkg/tsdb/elasticsearch/data_query_validator.go b/pkg/tsdb/elasticsearch/data_query_validator.go index def537c02da..72bcde016b6 100644 --- a/pkg/tsdb/elasticsearch/data_query_validator.go +++ b/pkg/tsdb/elasticsearch/data_query_validator.go @@ -6,6 +6,10 @@ import ( // isQueryWithError validates the query and returns an error if invalid func isQueryWithError(query *Query) error { + // Skip validation for raw DSL queries because no easy way to see it is valid without just running it + if query.EditorType != nil && *query.EditorType == "code" { + return nil + } if len(query.BucketAggs) == 0 { // If no aggregations, only document and logs queries are valid if len(query.Metrics) == 0 || (!isLogsQuery(query) && !isDocumentQuery(query)) { diff --git a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go index 175a486589c..31583f96f79 100644 --- a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go @@ -775,8 +775,12 @@ type ElasticsearchDataQuery struct { Alias *string `json:"alias,omitempty"` // Lucene query Query *string `json:"query,omitempty"` + // Raw DSL query + RawDSLQuery *string `json:"rawDSLQuery,omitempty"` // Name of time field TimeField *string `json:"timeField,omitempty"` + // Editor type + EditorType *string `json:"editorType,omitempty"` // List of bucket aggregations BucketAggs []BucketAggregation `json:"bucketAggs,omitempty"` // List of metric aggregations diff --git a/pkg/tsdb/elasticsearch/models.go b/pkg/tsdb/elasticsearch/models.go index d03861d3943..adb18554339 100644 --- a/pkg/tsdb/elasticsearch/models.go +++ b/pkg/tsdb/elasticsearch/models.go @@ -10,6 +10,7 @@ import ( // Query represents the time series query model of the datasource type Query struct { RawQuery string `json:"query"` + RawDSLQuery string `json:"rawDSLQuery"` BucketAggs []*BucketAgg `json:"bucketAggs"` Metrics []*MetricAgg `json:"metrics"` Alias string `json:"alias"` @@ -18,6 +19,7 @@ type Query struct { RefID string MaxDataPoints int64 TimeRange backend.TimeRange + EditorType *string `json:"editorType"` } // BucketAgg represents a bucket aggregation of the time series query model of the datasource diff --git a/pkg/tsdb/elasticsearch/parse_query.go b/pkg/tsdb/elasticsearch/parse_query.go index 27b9bc9b2e8..e1bfa189ab9 100644 --- a/pkg/tsdb/elasticsearch/parse_query.go +++ b/pkg/tsdb/elasticsearch/parse_query.go @@ -21,6 +21,13 @@ func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, err // please do not create a new field with that name, to avoid potential problems with old, persisted queries. rawQuery := model.Get("query").MustString() + rawDSLQuery := model.Get("rawDSLQuery").MustString() + + var editorType *string + if et := model.Get("editorType").MustString(); et != "" { + editorType = &et + } + bucketAggs, err := parseBucketAggs(model) if err != nil { logger.Error("Failed to parse bucket aggs in query", "error", err, "model", string(q.JSON)) @@ -37,6 +44,7 @@ func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, err queries = append(queries, &Query{ RawQuery: rawQuery, + RawDSLQuery: rawDSLQuery, BucketAggs: bucketAggs, Metrics: metrics, Alias: alias, @@ -45,6 +53,7 @@ func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, err RefID: q.RefID, MaxDataPoints: q.MaxDataPoints, TimeRange: q.TimeRange, + EditorType: editorType, }) } diff --git a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go new file mode 100644 index 00000000000..b092763b57d --- /dev/null +++ b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go @@ -0,0 +1,628 @@ +package elasticsearch + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + +// AggregationParser parses raw Elasticsearch DSL aggregations +type AggregationParser interface { + Parse(rawQuery string) ([]*BucketAgg, []*MetricAgg, error) +} + +// aggregationTypeParser handles parsing of specific aggregation types +type aggregationTypeParser interface { + CanParse(aggType string) bool + Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) +} + +type AggType string + +const ( + aggTypeBucket = AggType("bucket") + aggTypeMetric = AggType("metric") +) + +type dslAgg struct { + Field string `json:"field"` + Hide bool `json:"hide"` + ID string `json:"id"` + PipelineAggregate string `json:"pipelineAgg"` + PipelineVariables map[string]string `json:"pipelineVariables"` + Settings *simplejson.Json `json:"settings"` + Meta *simplejson.Json `json:"meta"` + Type string `json:"type"` + AggType AggType +} + +func (a *dslAgg) toBucketAgg() *BucketAgg { + return &BucketAgg{ + Field: a.Field, + ID: a.ID, + Settings: a.Settings, + Type: a.Type, + } +} + +func (a *dslAgg) toMetricAgg() *MetricAgg { + return &MetricAgg{ + Field: a.Field, + Hide: a.Hide, + ID: a.ID, + PipelineAggregate: a.PipelineAggregate, + PipelineVariables: a.PipelineVariables, + Settings: a.Settings, + Meta: a.Meta, + Type: a.Type, + } +} + +// fieldExtractor handles extracting and converting field values +type fieldExtractor struct{} + +func (e *fieldExtractor) getString(data map[string]any, key string) string { + if val, ok := data[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (e *fieldExtractor) getInt(data map[string]any, key string) int { + if val, ok := data[key]; ok { + switch v := val.(type) { + case float64: + return int(v) + case int: + return v + case string: + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + } + return 0 +} + +func (e *fieldExtractor) getFloat(data map[string]any, key string) float64 { + if val, ok := data[key]; ok { + switch v := val.(type) { + case float64: + return v + case int: + return float64(v) + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + } + return 0 +} + +func (e *fieldExtractor) getMap(data map[string]any, key string) map[string]any { + if val, ok := data[key]; ok { + if m, ok := val.(map[string]any); ok { + return m + } + } + return nil +} + +func (e *fieldExtractor) getSettings(data map[string]any) *simplejson.Json { + settings := make(map[string]any) + for k, v := range data { + // Skip known non-setting fields + if k == "field" || k == "buckets_path" { + continue + } + settings[k] = v + } + return simplejson.NewFromAny(settings) +} + +// dateHistogramParser handles date_histogram aggregations +type dateHistogramParser struct { + extractor *fieldExtractor +} + +func (p *dateHistogramParser) CanParse(aggType string) bool { + return aggType == dateHistType +} + +func (p *dateHistogramParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + + settings := make(map[string]any) + if interval := p.extractor.getString(aggValue, "fixed_interval"); interval != "" { + settings["interval"] = interval + } else if interval := p.extractor.getString(aggValue, "calendar_interval"); interval != "" { + settings["interval"] = interval + } else if interval := p.extractor.getString(aggValue, "interval"); interval != "" { + settings["interval"] = interval + } + + if minDocCount := p.extractor.getInt(aggValue, "min_doc_count"); minDocCount > 0 { + settings["min_doc_count"] = strconv.Itoa(minDocCount) + } + + if timeZone := p.extractor.getString(aggValue, "time_zone"); timeZone != "" { + settings["time_zone"] = timeZone + } + + return &dslAgg{ + ID: id, + Type: dateHistType, + Field: field, + Settings: simplejson.NewFromAny(settings), + AggType: aggTypeBucket, + }, nil +} + +// termsParser handles terms aggregations +type termsParser struct { + extractor *fieldExtractor +} + +func (p *termsParser) CanParse(aggType string) bool { + return aggType == termsType +} + +func (p *termsParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + + settings := make(map[string]any) + if size := p.extractor.getInt(aggValue, "size"); size > 0 { + settings["size"] = strconv.Itoa(size) + } + + if order := p.extractor.getMap(aggValue, "order"); order != nil { + for k := range order { + settings["orderBy"] = k + orderJSON := p.extractor.getString(order, k) + settings["order"] = orderJSON + } + } + + if minDocCount := p.extractor.getInt(aggValue, "min_doc_count"); minDocCount != 0 { + minDocCountJSON, _ := json.Marshal(minDocCount) + settings["min_doc_count"] = string(minDocCountJSON) + } + + if missing := p.extractor.getString(aggValue, "missing"); missing != "" { + settings["missing"] = missing + } + + return &dslAgg{ + ID: id, + Type: termsType, + Field: field, + Settings: simplejson.NewFromAny(settings), + AggType: aggTypeBucket, + }, nil +} + +// histogramParser handles histogram aggregations +type histogramParser struct { + extractor *fieldExtractor +} + +func (p *histogramParser) CanParse(aggType string) bool { + return aggType == histogramType +} + +func (p *histogramParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + + settings := make(map[string]any) + if interval := p.extractor.getFloat(aggValue, "interval"); interval > 0 { + settings["interval"] = strconv.FormatFloat(interval, 'f', -1, 64) + } + + if minDocCount := p.extractor.getInt(aggValue, "min_doc_count"); minDocCount > 0 { + settings["min_doc_count"] = strconv.Itoa(minDocCount) + } + + return &dslAgg{ + ID: id, + Type: histogramType, + Field: field, + Settings: simplejson.NewFromAny(settings), + AggType: aggTypeBucket, + }, nil +} + +// simpleMetricParser handles simple metric aggregations (avg, sum, min, max, cardinality) +type simpleMetricParser struct { + extractor *fieldExtractor + types map[string]bool +} + +func newSimpleMetricParser() *simpleMetricParser { + return &simpleMetricParser{ + extractor: &fieldExtractor{}, + types: map[string]bool{ + "avg": true, + "sum": true, + "min": true, + "max": true, + "cardinality": true, + }, + } +} + +func (p *simpleMetricParser) CanParse(aggType string) bool { + return p.types[aggType] +} + +func (p *simpleMetricParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + settings := p.extractor.getSettings(aggValue) + + return &dslAgg{ + ID: id, + Type: aggType, + Field: field, + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// filtersParser handles filters aggregations +type filtersParser struct { + extractor *fieldExtractor +} + +func (p *filtersParser) CanParse(aggType string) bool { + return aggType == filtersType +} + +func (p *filtersParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + settings := make(map[string]any) + + if filters := p.extractor.getMap(aggValue, "filters"); filters != nil { + filtersArray := make([]any, 0, len(filters)) + for k, v := range filters { + if queryString := p.extractor.getMap(v.(map[string]any), "query_string"); queryString != nil { + queryString["label"] = k + filtersArray = append(filtersArray, queryString) + } + } + settings["filters"] = filtersArray + } + + return &dslAgg{ + ID: id, + Type: filtersType, + Field: "", + Settings: simplejson.NewFromAny(settings), + AggType: aggTypeBucket, + }, nil +} + +// geohashGridParser handles geohash_grid aggregations +type geohashGridParser struct { + extractor *fieldExtractor +} + +func (p *geohashGridParser) CanParse(aggType string) bool { + return aggType == geohashGridType +} + +func (p *geohashGridParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + + settings := make(map[string]any) + if precision := p.extractor.getInt(aggValue, "precision"); precision > 0 { + settings["precision"] = strconv.Itoa(precision) + } + + return &dslAgg{ + ID: id, + Type: geohashGridType, + Field: field, + Settings: simplejson.NewFromAny(settings), + AggType: aggTypeBucket, + }, nil +} + +// nestedParser handles nested aggregations +type nestedParser struct { + extractor *fieldExtractor +} + +func (p *nestedParser) CanParse(aggType string) bool { + return aggType == nestedType +} + +func (p *nestedParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + path := p.extractor.getString(aggValue, "path") + + return &dslAgg{ + ID: id, + Type: nestedType, + Field: path, + Settings: simplejson.NewFromAny(map[string]any{}), + AggType: aggTypeBucket, + }, nil +} + +// extendedStatsParser handles extended_stats aggregations +type extendedStatsParser struct { + extractor *fieldExtractor +} + +func (p *extendedStatsParser) CanParse(aggType string) bool { + return aggType == extendedStatsType +} + +func (p *extendedStatsParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + settings := p.extractor.getSettings(aggValue) + + return &dslAgg{ + ID: id, + Type: extendedStatsType, + Field: field, + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// percentilesParser handles percentiles aggregations +type percentilesParser struct { + extractor *fieldExtractor +} + +func (p *percentilesParser) CanParse(aggType string) bool { + return aggType == percentilesType +} + +func (p *percentilesParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + field := p.extractor.getString(aggValue, "field") + settings := p.extractor.getSettings(aggValue) + + return &dslAgg{ + ID: id, + Type: percentilesType, + Field: field, + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// topMetricsParser handles top_metrics aggregations +type topMetricsParser struct { + extractor *fieldExtractor +} + +func (p *topMetricsParser) CanParse(aggType string) bool { + return aggType == topMetricsType +} + +func (p *topMetricsParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + settings := p.extractor.getSettings(aggValue) + + // Extract metrics field if present + field := "" + if metrics := p.extractor.getMap(aggValue, "metrics"); metrics != nil { + if metricsField := p.extractor.getString(metrics, "field"); metricsField != "" { + field = metricsField + } + } + + return &dslAgg{ + ID: id, + Type: topMetricsType, + Field: field, + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// pipelineParser handles pipeline aggregations +type pipelineParser struct { + extractor *fieldExtractor + types map[string]bool +} + +func newPipelineParser() *pipelineParser { + return &pipelineParser{ + extractor: &fieldExtractor{}, + types: map[string]bool{ + "moving_avg": true, + "moving_fn": true, + "derivative": true, + "cumulative_sum": true, + "serial_diff": true, + }, + } +} + +func (p *pipelineParser) CanParse(aggType string) bool { + return p.types[aggType] +} + +func (p *pipelineParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + bucketsPath := p.extractor.getString(aggValue, "buckets_path") + settings := p.extractor.getSettings(aggValue) + + return &dslAgg{ + ID: id, + Type: aggType, + Field: bucketsPath, // For pipeline aggs, buckets_path goes in Field + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// bucketScriptParser handles bucket_script aggregations +type bucketScriptParser struct { + extractor *fieldExtractor +} + +func (p *bucketScriptParser) CanParse(aggType string) bool { + return aggType == "bucket_script" +} + +func (p *bucketScriptParser) Parse(id, aggType string, aggValue map[string]any) (*dslAgg, error) { + settings := p.extractor.getSettings(aggValue) + + // Extract buckets_path (can be a string or map) + pipelineVariables := make(map[string]string) + if bucketsPath, ok := aggValue["buckets_path"]; ok { + switch bp := bucketsPath.(type) { + case string: + // Single string bucket path + pipelineVariables["var1"] = bp + case map[string]any: + // Map of variable names to bucket paths + for varName, path := range bp { + if pathStr, ok := path.(string); ok { + pipelineVariables[varName] = pathStr + } + } + } + } + + return &dslAgg{ + ID: id, + Type: "bucket_script", + Field: "", + PipelineVariables: pipelineVariables, + Settings: settings, + AggType: aggTypeMetric, + }, nil +} + +// compositeParser combines multiple parsers +type compositeParser struct { + parsers []aggregationTypeParser + extractor *fieldExtractor +} + +func newCompositeParser() *compositeParser { + extractor := &fieldExtractor{} + return &compositeParser{ + extractor: extractor, + parsers: []aggregationTypeParser{ + // Bucket aggregations + &dateHistogramParser{extractor: extractor}, + &termsParser{extractor: extractor}, + &histogramParser{extractor: extractor}, + &filtersParser{extractor: extractor}, + &geohashGridParser{extractor: extractor}, + &nestedParser{extractor: extractor}, + // Metric aggregations + newSimpleMetricParser(), + &extendedStatsParser{extractor: extractor}, + &percentilesParser{extractor: extractor}, + &topMetricsParser{extractor: extractor}, + + // Pipeline aggregations + newPipelineParser(), + &bucketScriptParser{extractor: extractor}, + }, + } +} + +func (p *compositeParser) findParser(aggType string) aggregationTypeParser { + for _, parser := range p.parsers { + if parser.CanParse(aggType) { + return parser + } + } + return nil +} + +func (p *compositeParser) Parse(rawQuery string) ([]*BucketAgg, []*MetricAgg, error) { + if rawQuery == "" { + return nil, nil, nil + } + + var queryBody map[string]any + if err := json.Unmarshal([]byte(rawQuery), &queryBody); err != nil { + return nil, nil, fmt.Errorf("failed to parse raw query JSON: %w", err) + } + + // Look for aggregations in both "aggs" and "aggregations" + var aggsData map[string]any + if aggs, ok := queryBody["aggs"].(map[string]any); ok { + aggsData = aggs + } else if aggs, ok := queryBody["aggregations"].(map[string]any); ok { + aggsData = aggs + } + + if aggsData == nil { + return nil, nil, nil + } + + b, m := p.parseAggregations(aggsData) + return b, m, nil +} + +func (p *compositeParser) parseAggregations(aggsData map[string]any) ([]*BucketAgg, []*MetricAgg) { + var bucketAggs []*BucketAgg + var metricAggs []*MetricAgg + + for aggID, aggData := range aggsData { + aggMap, ok := aggData.(map[string]any) + if !ok { + continue + } + + // Find the aggregation type (first key that's not "aggs" or "aggregations") + var aggType string + var aggValue map[string]any + for key, value := range aggMap { + if key != "aggs" && key != "aggregations" { + aggType = key + if val, ok := value.(map[string]any); ok { + aggValue = val + } + break + } + } + + if aggType == "" || aggValue == nil { + continue + } + + // Find the appropriate parser for this aggregation type + parser := p.findParser(aggType) + if parser == nil { + // Unknown aggregation type, skip it + continue + } + + // Try to parse as agg aggregation + if agg, err := parser.Parse(aggID, aggType, aggValue); err == nil && agg != nil { + switch agg.AggType { + case aggTypeBucket: + bucketAggs = append(bucketAggs, agg.toBucketAgg()) + case aggTypeMetric: + metricAggs = append(metricAggs, agg.toMetricAgg()) + } + } + + // Parse nested aggregations + nestedAggs := p.extractor.getMap(aggMap, "aggs") + if nestedAggs == nil { + nestedAggs = p.extractor.getMap(aggMap, "aggregations") + } + nestedBuckets, nestedMetrics := p.parseAggregations(nestedAggs) + bucketAggs = append(bucketAggs, nestedBuckets...) + metricAggs = append(metricAggs, nestedMetrics...) + } + + return bucketAggs, metricAggs +} + +// NewAggregationParser creates a new aggregation parser +func NewAggregationParser() AggregationParser { + return newCompositeParser() +} diff --git a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser_test.go b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser_test.go new file mode 100644 index 00000000000..c8ad0c9ef6e --- /dev/null +++ b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser_test.go @@ -0,0 +1,706 @@ +package elasticsearch + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestFieldExtractor tests the field extraction utility +func TestFieldExtractor(t *testing.T) { + extractor := &fieldExtractor{} + + t.Run("getString", func(t *testing.T) { + data := map[string]any{ + "field": "value", + "number": 42, + "missing": nil, + } + + assert.Equal(t, "value", extractor.getString(data, "field")) + assert.Equal(t, "", extractor.getString(data, "number")) + assert.Equal(t, "", extractor.getString(data, "missing")) + assert.Equal(t, "", extractor.getString(data, "nonexistent")) + }) + + t.Run("getInt", func(t *testing.T) { + data := map[string]any{ + "float": 42.0, + "int": 100, + "string": "200", + "bad": "notanumber", + } + + assert.Equal(t, 42, extractor.getInt(data, "float")) + assert.Equal(t, 100, extractor.getInt(data, "int")) + assert.Equal(t, 200, extractor.getInt(data, "string")) + assert.Equal(t, 0, extractor.getInt(data, "bad")) + assert.Equal(t, 0, extractor.getInt(data, "nonexistent")) + }) + + t.Run("getFloat", func(t *testing.T) { + data := map[string]any{ + "float": 42.5, + "int": 100, + "string": "3.14", + } + + assert.Equal(t, 42.5, extractor.getFloat(data, "float")) + assert.Equal(t, 100.0, extractor.getFloat(data, "int")) + assert.Equal(t, 3.14, extractor.getFloat(data, "string")) + assert.Equal(t, 0.0, extractor.getFloat(data, "nonexistent")) + }) + + t.Run("getMap", func(t *testing.T) { + data := map[string]any{ + "map": map[string]any{"key": "value"}, + "notmap": "string", + } + + result := extractor.getMap(data, "map") + require.NotNil(t, result) + assert.Equal(t, "value", result["key"]) + + assert.Nil(t, extractor.getMap(data, "notmap")) + assert.Nil(t, extractor.getMap(data, "nonexistent")) + }) +} + +// TestDateHistogramParser tests the date histogram parser +func TestDateHistogramParser(t *testing.T) { + parser := &dateHistogramParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(dateHistType)) + assert.False(t, parser.CanParse("terms")) + }) + + t.Run("Parse with fixed_interval", func(t *testing.T) { + aggValue := map[string]any{ + "field": "@timestamp", + "fixed_interval": "30s", + "min_doc_count": 1, + } + + agg, err := parser.Parse("1", dateHistType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + bucket := agg.toBucketAgg() + assert.Equal(t, "1", bucket.ID) + assert.Equal(t, dateHistType, bucket.Type) + assert.Equal(t, "@timestamp", bucket.Field) + assert.Equal(t, "30s", bucket.Settings.Get("interval").MustString()) + assert.Equal(t, "1", bucket.Settings.Get("min_doc_count").MustString()) + }) + + t.Run("Parse with calendar_interval", func(t *testing.T) { + aggValue := map[string]any{ + "field": "@timestamp", + "calendar_interval": "1d", + "time_zone": "UTC", + } + + agg, err := parser.Parse("2", dateHistType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + bucket := agg.toBucketAgg() + assert.Equal(t, "1d", bucket.Settings.Get("interval").MustString()) + assert.Equal(t, "UTC", bucket.Settings.Get("time_zone").MustString()) + }) + + t.Run("Parse returns bucket aggregation", func(t *testing.T) { + agg, err := parser.Parse("1", dateHistType, map[string]any{"field": "@timestamp"}) + assert.NoError(t, err) + assert.NotNil(t, agg) + assert.Equal(t, aggTypeBucket, agg.AggType) + }) +} + +// TestTermsParser tests the terms parser +func TestTermsParser(t *testing.T) { + parser := &termsParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(termsType)) + assert.False(t, parser.CanParse("histogram")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "field": "hostname.keyword", + "size": 10, + "order": map[string]any{"_count": "desc"}, + } + + agg, err := parser.Parse("3", termsType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + bucket := agg.toBucketAgg() + assert.Equal(t, "3", bucket.ID) + assert.Equal(t, termsType, bucket.Type) + assert.Equal(t, "hostname.keyword", bucket.Field) + assert.Equal(t, "10", bucket.Settings.Get("size").MustString()) + }) +} + +// TestHistogramParser tests the histogram parser +func TestHistogramParser(t *testing.T) { + parser := &histogramParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(histogramType)) + assert.False(t, parser.CanParse("terms")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "field": "response_time", + "interval": 50.0, + } + + agg, err := parser.Parse("4", histogramType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + bucket := agg.toBucketAgg() + assert.Equal(t, "4", bucket.ID) + assert.Equal(t, histogramType, bucket.Type) + assert.Equal(t, "response_time", bucket.Field) + assert.Equal(t, "50", bucket.Settings.Get("interval").MustString()) + }) +} + +// TestFiltersParser tests the filters parser +func TestFiltersParser(t *testing.T) { + parser := &filtersParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(filtersType)) + assert.False(t, parser.CanParse("terms")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "filters": map[string]any{ + "errors": map[string]any{"query_string": map[string]any{"query": "level:error"}}, + "warnings": map[string]any{"query_string": map[string]any{"query": "level:warning"}}, + }, + } + + agg, err := parser.Parse("filters", filtersType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + bucket := agg.toBucketAgg() + assert.Equal(t, "filters", bucket.ID) + assert.Equal(t, filtersType, bucket.Type) + filtersArray := bucket.Settings.Get("filters").MustArray() + assert.NotEmpty(t, filtersArray) + assert.Len(t, filtersArray, 2) + }) +} + +// TestSimpleMetricParser tests the simple metric parser +func TestSimpleMetricParser(t *testing.T) { + parser := newSimpleMetricParser() + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse("avg")) + assert.True(t, parser.CanParse("sum")) + assert.True(t, parser.CanParse("min")) + assert.True(t, parser.CanParse("max")) + assert.True(t, parser.CanParse("cardinality")) + assert.False(t, parser.CanParse("bucket_script")) + }) + + t.Run("Parse avg", func(t *testing.T) { + aggValue := map[string]any{ + "field": "response_time", + } + + agg, err := parser.Parse("1", "avg", aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "1", metric.ID) + assert.Equal(t, "avg", metric.Type) + assert.Equal(t, "response_time", metric.Field) + }) + + t.Run("Parse returns metric aggregation", func(t *testing.T) { + agg, err := parser.Parse("1", "avg", map[string]any{}) + assert.NoError(t, err) + assert.NotNil(t, agg) + assert.Equal(t, aggTypeMetric, agg.AggType) + }) +} + +// TestExtendedStatsParser tests the extended stats parser +func TestExtendedStatsParser(t *testing.T) { + parser := &extendedStatsParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(extendedStatsType)) + assert.False(t, parser.CanParse("avg")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "field": "response_time", + "sigma": 2, + } + + agg, err := parser.Parse("stats", extendedStatsType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "stats", metric.ID) + assert.Equal(t, extendedStatsType, metric.Type) + assert.Equal(t, "response_time", metric.Field) + }) +} + +// TestPercentilesParser tests the percentiles parser +func TestPercentilesParser(t *testing.T) { + parser := &percentilesParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse(percentilesType)) + assert.False(t, parser.CanParse("avg")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "field": "response_time", + "percents": []any{50.0, 95.0, 99.0}, + } + + agg, err := parser.Parse("percentiles", percentilesType, aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "percentiles", metric.ID) + assert.Equal(t, percentilesType, metric.Type) + assert.Equal(t, "response_time", metric.Field) + }) +} + +// TestPipelineParser tests the pipeline parser +func TestPipelineParser(t *testing.T) { + parser := newPipelineParser() + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse("moving_avg")) + assert.True(t, parser.CanParse("derivative")) + assert.True(t, parser.CanParse("cumulative_sum")) + assert.False(t, parser.CanParse("bucket_script")) + }) + + t.Run("Parse", func(t *testing.T) { + aggValue := map[string]any{ + "buckets_path": "1", + } + + agg, err := parser.Parse("moving", "moving_avg", aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "moving", metric.ID) + assert.Equal(t, "moving_avg", metric.Type) + assert.Equal(t, "1", metric.Field) + }) +} + +// TestBucketScriptParser tests the bucket script parser +func TestBucketScriptParser(t *testing.T) { + parser := &bucketScriptParser{extractor: &fieldExtractor{}} + + t.Run("CanParse", func(t *testing.T) { + assert.True(t, parser.CanParse("bucket_script")) + assert.False(t, parser.CanParse("moving_avg")) + }) + + t.Run("Parse with map buckets_path", func(t *testing.T) { + aggValue := map[string]any{ + "buckets_path": map[string]any{ + "count": "total", + }, + "script": "params.count / 60", + } + + agg, err := parser.Parse("rate", "bucket_script", aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "rate", metric.ID) + assert.Equal(t, "bucket_script", metric.Type) + assert.Equal(t, "total", metric.PipelineVariables["count"]) + assert.Equal(t, "params.count / 60", metric.Settings.Get("script").MustString()) + }) + + t.Run("Parse with string buckets_path", func(t *testing.T) { + aggValue := map[string]any{ + "buckets_path": "1", + } + + agg, err := parser.Parse("rate", "bucket_script", aggValue) + require.NoError(t, err) + require.NotNil(t, agg) + + metric := agg.toMetricAgg() + assert.Equal(t, "1", metric.PipelineVariables["var1"]) + }) +} + +// TestCompositeParser tests the full parser integration +func TestCompositeParser(t *testing.T) { + parser := NewAggregationParser() + + t.Run("Parse date histogram aggregation", func(t *testing.T) { + rawQuery := `{ + "query": { + "match_all": {} + }, + "aggs": { + "2": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "30s", + "min_doc_count": 1 + } + } + } + }` + + bucketAggs, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + require.Len(t, metricAggs, 0) + + assert.Equal(t, "2", bucketAggs[0].ID) + assert.Equal(t, dateHistType, bucketAggs[0].Type) + assert.Equal(t, "@timestamp", bucketAggs[0].Field) + assert.Equal(t, "30s", bucketAggs[0].Settings.Get("interval").MustString()) + }) + + t.Run("Parse nested aggregations with metrics", func(t *testing.T) { + rawQuery := `{ + "query": { + "match_all": {} + }, + "aggs": { + "2": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "30s" + }, + "aggs": { + "1": { + "avg": { + "field": "value" + } + }, + "3": { + "sum": { + "field": "total" + } + } + } + } + } + }` + + bucketAggs, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + require.Len(t, metricAggs, 2) + + // Check bucket aggregation + assert.Equal(t, "2", bucketAggs[0].ID) + assert.Equal(t, dateHistType, bucketAggs[0].Type) + + // Check metric aggregations + avgFound := false + sumFound := false + for _, m := range metricAggs { + if m.ID == "1" && m.Type == "avg" && m.Field == "value" { + avgFound = true + } + if m.ID == "3" && m.Type == "sum" && m.Field == "total" { + sumFound = true + } + } + assert.True(t, avgFound, "avg aggregation not found") + assert.True(t, sumFound, "sum aggregation not found") + }) + + t.Run("Parse terms aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "3": { + "terms": { + "field": "hostname.keyword", + "size": 10, + "order": { + "_count": "desc" + } + } + } + } + }` + + bucketAggs, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + require.Len(t, metricAggs, 0) + + assert.Equal(t, "3", bucketAggs[0].ID) + assert.Equal(t, termsType, bucketAggs[0].Type) + assert.Equal(t, "hostname.keyword", bucketAggs[0].Field) + assert.Equal(t, "10", bucketAggs[0].Settings.Get("size").MustString()) + }) + + t.Run("Parse histogram aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "4": { + "histogram": { + "field": "response_time", + "interval": 50 + } + } + } + }` + + bucketAggs, _, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + + assert.Equal(t, "4", bucketAggs[0].ID) + assert.Equal(t, histogramType, bucketAggs[0].Type) + assert.Equal(t, "response_time", bucketAggs[0].Field) + assert.Equal(t, "50", bucketAggs[0].Settings.Get("interval").MustString()) + }) + + t.Run("Parse extended stats aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "stats": { + "extended_stats": { + "field": "response_time", + "sigma": 2 + } + } + } + }` + + _, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, metricAggs, 1) + + assert.Equal(t, "stats", metricAggs[0].ID) + assert.Equal(t, extendedStatsType, metricAggs[0].Type) + assert.Equal(t, "response_time", metricAggs[0].Field) + }) + + t.Run("Parse percentiles aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "percentiles": { + "percentiles": { + "field": "response_time", + "percents": [50, 95, 99] + } + } + } + }` + + _, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, metricAggs, 1) + + assert.Equal(t, "percentiles", metricAggs[0].ID) + assert.Equal(t, percentilesType, metricAggs[0].Type) + }) + + t.Run("Parse pipeline aggregations", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "2": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "1m" + }, + "aggs": { + "1": { + "avg": { + "field": "value" + } + }, + "moving": { + "moving_avg": { + "buckets_path": "1" + } + }, + "deriv": { + "derivative": { + "buckets_path": "1" + } + } + } + } + } + }` + + bucketAggs, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + require.GreaterOrEqual(t, len(metricAggs), 2) // At least avg and one pipeline + + // Find pipeline aggregations + movingAvgFound := false + derivativeFound := false + for _, m := range metricAggs { + if m.ID == "moving" && m.Type == "moving_avg" { + movingAvgFound = true + assert.Equal(t, "1", m.Field) + } + if m.ID == "deriv" && m.Type == "derivative" { + derivativeFound = true + assert.Equal(t, "1", m.Field) + } + } + assert.True(t, movingAvgFound, "moving_avg aggregation not found") + assert.True(t, derivativeFound, "derivative aggregation not found") + }) + + t.Run("Parse bucket script aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "2": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "1m" + }, + "aggs": { + "total": { + "sum": { + "field": "bytes" + } + }, + "rate": { + "bucket_script": { + "buckets_path": { + "count": "total" + }, + "script": "params.count / 60" + } + } + } + } + } + }` + + _, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + + // Find bucket script + var bucketScriptAgg *MetricAgg + for _, m := range metricAggs { + if m.ID == "rate" && m.Type == "bucket_script" { + bucketScriptAgg = m + break + } + } + require.NotNil(t, bucketScriptAgg, "bucket_script aggregation not found") + assert.Equal(t, "params.count / 60", bucketScriptAgg.Settings.Get("script").MustString()) + assert.Equal(t, "total", bucketScriptAgg.PipelineVariables["count"]) + }) + + t.Run("Parse filters aggregation", func(t *testing.T) { + rawQuery := `{ + "aggs": { + "messages": { + "filters": { + "filters": { + "errors": { + "query_string": { + "query": "level:error" + } + }, + "warnings": { + "query_string": { + "query": "level:warning" + } + } + } + } + } + } + }` + + bucketAggs, _, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + + assert.Equal(t, "messages", bucketAggs[0].ID) + assert.Equal(t, filtersType, bucketAggs[0].Type) + }) + + t.Run("Handle empty query", func(t *testing.T) { + bucketAggs, metricAggs, err := parser.Parse("") + require.NoError(t, err) + assert.Nil(t, bucketAggs) + assert.Nil(t, metricAggs) + }) + + t.Run("Handle query without aggregations", func(t *testing.T) { + rawQuery := `{ + "query": { + "match_all": {} + } + }` + + bucketAggs, metricAggs, err := parser.Parse(rawQuery) + require.NoError(t, err) + assert.Nil(t, bucketAggs) + assert.Nil(t, metricAggs) + }) + + t.Run("Handle invalid JSON", func(t *testing.T) { + rawQuery := `{invalid json` + + _, _, err := parser.Parse(rawQuery) + require.Error(t, err) + }) + + t.Run("Use 'aggregations' instead of 'aggs'", func(t *testing.T) { + rawQuery := `{ + "query": { + "match_all": {} + }, + "aggregations": { + "2": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "30s" + } + } + } + }` + + bucketAggs, _, err := parser.Parse(rawQuery) + require.NoError(t, err) + require.Len(t, bucketAggs, 1) + assert.Equal(t, "2", bucketAggs[0].ID) + }) +} diff --git a/pkg/tsdb/opentsdb/callresource.go b/pkg/tsdb/opentsdb/callresource.go new file mode 100644 index 00000000000..be0f81b9c80 --- /dev/null +++ b/pkg/tsdb/opentsdb/callresource.go @@ -0,0 +1,67 @@ +package opentsdb + +import ( + "fmt" + "net/http" + "net/url" + "path" + + "github.com/grafana/grafana-plugin-sdk-go/backend" +) + +func (s *Service) HandleSuggestQuery(rw http.ResponseWriter, req *http.Request) { + logger := logger.FromContext(req.Context()) + + dsInfo, err := s.getDSInfo(req.Context(), backend.PluginConfigFromContext(req.Context())) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to get datasource info: %v", err), http.StatusInternalServerError) + return + } + + u, err := url.Parse(dsInfo.URL) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse datasource URL: %v", err), http.StatusInternalServerError) + return + } + + u.Path = path.Join(u.Path, "api/suggest") + u.RawQuery = req.URL.RawQuery + httpReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, u.String(), nil) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to create request: %v", err), http.StatusInternalServerError) + return + } + + res, err := dsInfo.HTTPClient.Do(httpReq) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to execute request: %v", err), http.StatusInternalServerError) + return + } + + defer func() { + if err := res.Body.Close(); err != nil { + logger.Error("Failed to close response body", "error", err) + } + }() + + responseBody, err := DecodeResponseBody(res, logger) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to decode response: %v", err), http.StatusInternalServerError) + return + } + + for name, values := range res.Header { + if name == "Content-Encoding" || name == "Content-Length" { + continue + } + for _, value := range values { + rw.Header().Add(name, value) + } + } + + rw.WriteHeader(res.StatusCode) + if _, err := rw.Write(responseBody); err != nil { + logger.Error("Failed to write response", "error", err) + return + } +} diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 8b34b21cbaf..a694445e1cd 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -4,21 +4,15 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" "path" - "sort" - "strconv" - "strings" - "time" "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-plugin-sdk-go/backend/resource/httpadapter" ) var logger = backend.NewLoggerWith("tsdb.opentsdb") @@ -62,6 +56,7 @@ type QueryModel struct { IsCounter bool `json:"isCounter"` CounterMax string `json:"counterMax"` CounterResetValue string `json:"counterResetValue"` + ExplicitTags bool `json:"explicitTags"` } func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { @@ -154,6 +149,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque }, nil } +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + mux := http.NewServeMux() + mux.HandleFunc("/api/suggest", s.HandleSuggestQuery) + + handler := httpadapter.New(mux) + return handler.CallResource(ctx, req, sender) +} + func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { logger := logger.FromContext(ctx) @@ -165,16 +168,15 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) result := backend.NewQueryDataResponse() for _, query := range req.Queries { - // Build OpenTsdbQuery with per-query time range tsdbQuery := OpenTsdbQuery{ Start: query.TimeRange.From.Unix(), End: query.TimeRange.To.Unix(), Queries: []map[string]any{ - s.buildMetric(query), + BuildMetric(query), }, } - httpReq, err := s.createRequest(ctx, logger, dsInfo, tsdbQuery) + httpReq, err := CreateRequest(ctx, logger, dsInfo, tsdbQuery) if err != nil { return nil, err } @@ -190,227 +192,17 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) } }() - queryRes, err := s.parseResponse(logger, httpRes, query.RefID, dsInfo.TSDBVersion) + queryRes, err := ParseResponse(logger, httpRes, query.RefID, dsInfo.TSDBVersion) if err != nil { return nil, err } - // Attach parsed result for this query's RefID result.Responses[query.RefID] = queryRes.Responses[query.RefID] } return result, nil } -func (s *Service) createRequest(ctx context.Context, logger log.Logger, dsInfo *datasourceInfo, data OpenTsdbQuery) (*http.Request, error) { - u, err := url.Parse(dsInfo.URL) - if err != nil { - return nil, err - } - u.Path = path.Join(u.Path, "api/query") - if dsInfo.TSDBVersion == 4 { - queryParams := u.Query() - queryParams.Set("arrays", "true") - u.RawQuery = queryParams.Encode() - } - - postData, err := json.Marshal(data) - if err != nil { - logger.Info("Failed marshaling data", "error", err) - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(string(postData))) - if err != nil { - logger.Info("Failed to create request", "error", err) - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - return req, nil -} - -func createInitialFrame(val OpenTsdbCommon, length int, refID string) *data.Frame { - labels := data.Labels{} - for label, value := range val.Tags { - labels[label] = value - } - - frame := data.NewFrameOfFieldTypes(val.Metric, length, data.FieldTypeTime, data.FieldTypeFloat64) - frame.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}} - frame.RefID = refID - timeField := frame.Fields[0] - timeField.Name = data.TimeSeriesTimeFieldName - dataField := frame.Fields[1] - dataField.Name = val.Metric - dataField.Labels = labels - - return frame -} - -// Parse response function for OpenTSDB version 2.4 -func parseResponse24(responseData []OpenTsdbResponse24, refID string, frames data.Frames) data.Frames { - for _, val := range responseData { - frame := createInitialFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) - - for i, point := range val.DataPoints { - frame.SetRow(i, time.Unix(int64(point[0]), 0).UTC(), point[1]) - } - - frames = append(frames, frame) - } - - return frames -} - -// Parse response function for OpenTSDB versions < 2.4 -func parseResponseLT24(responseData []OpenTsdbResponse, refID string, frames data.Frames) (data.Frames, error) { - for _, val := range responseData { - frame := createInitialFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) - - // Order the timestamps in ascending order to avoid issues like https://github.com/grafana/grafana/issues/38729 - timestamps := make([]string, 0, len(val.DataPoints)) - for timestamp := range val.DataPoints { - timestamps = append(timestamps, timestamp) - } - sort.Strings(timestamps) - - for i, timeString := range timestamps { - timestamp, err := strconv.ParseInt(timeString, 10, 64) - if err != nil { - logger.Info("Failed to unmarshal opentsdb timestamp", "timestamp", timeString) - return frames, err - } - frame.SetRow(i, time.Unix(timestamp, 0).UTC(), val.DataPoints[timeString]) - } - - frames = append(frames, frame) - } - - return frames, nil -} - -func (s *Service) parseResponse(logger log.Logger, res *http.Response, refID string, tsdbVersion float32) (*backend.QueryDataResponse, error) { - resp := backend.NewQueryDataResponse() - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - defer func() { - if err := res.Body.Close(); err != nil { - logger.Warn("Failed to close response body", "err", err) - } - }() - - if res.StatusCode/100 != 2 { - logger.Info("Request failed", "status", res.Status, "body", string(body)) - return nil, fmt.Errorf("request failed, status: %s", res.Status) - } - - frames := data.Frames{} - - var responseData []OpenTsdbResponse - var responseData24 []OpenTsdbResponse24 - if tsdbVersion == 4 { - err = json.Unmarshal(body, &responseData24) - if err != nil { - logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) - return nil, err - } - - frames = parseResponse24(responseData24, refID, frames) - } else { - err = json.Unmarshal(body, &responseData) - if err != nil { - logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) - return nil, err - } - - frames, err = parseResponseLT24(responseData, refID, frames) - if err != nil { - return nil, err - } - } - - result := resp.Responses[refID] - result.Frames = frames - resp.Responses[refID] = result - return resp, nil -} - -func (s *Service) buildMetric(query backend.DataQuery) map[string]any { - metric := make(map[string]any) - - var model QueryModel - if err := json.Unmarshal(query.JSON, &model); err != nil { - return nil - } - - // Setting metric and aggregator - metric["metric"] = model.Metric - metric["aggregator"] = model.Aggregator - - // Setting downsampling options - if !model.DisableDownsampling { - downsampleInterval := model.DownsampleInterval - if downsampleInterval == "" { - downsampleInterval = "1m" // default value for blank - } - downsample := downsampleInterval + "-" + model.DownsampleAggregator - if model.DownsampleFillPolicy != "none" { - metric["downsample"] = downsample + "-" + model.DownsampleFillPolicy - } else { - metric["downsample"] = downsample - } - } - - // Setting rate options - if model.ShouldComputeRate { - metric["rate"] = true - rateOptions := make(map[string]any) - rateOptions["counter"] = model.IsCounter - - var counterMax *float64 - if model.CounterMax != "" { - if val, err := strconv.ParseFloat(model.CounterMax, 64); err == nil { - counterMax = &val - } - } - if counterMax != nil { - rateOptions["counterMax"] = *counterMax - } - - var counterResetValue *float64 - if model.CounterResetValue != "" { - if val, err := strconv.ParseFloat(model.CounterResetValue, 64); err == nil { - counterResetValue = &val - } - } - if counterResetValue != nil { - rateOptions["resetValue"] = *counterResetValue - } - - if counterMax == nil && (counterResetValue == nil || *counterResetValue == 0) { - rateOptions["dropResets"] = true - } - - metric["rateOptions"] = rateOptions - } - - // Setting tags - if len(model.Tags) > 0 { - metric["tags"] = model.Tags - } - - // Setting filters - if len(model.Filters) > 0 { - metric["filters"] = model.Filters - } - - return metric -} - func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext) (*datasourceInfo, error) { i, err := s.im.Get(ctx, pluginCtx) if err != nil { diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index b959e9efa26..72bbe6bf8b9 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -70,11 +70,165 @@ func TestCheckHealth(t *testing.T) { } } -func TestOpenTsdbExecutor(t *testing.T) { - service := &Service{} +func TestBuildMetric(t *testing.T) { + t.Run("Metric with no downsampleInterval should use query interval", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 30 * time.Second, + } + metric := BuildMetric(query) + require.Equal(t, "30s-avg", metric["downsample"], "should use query interval formatted as seconds") + }) + + t.Run("Metric with downsampleInterval converts decimal seconds to milliseconds", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "0.5s", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + } + + metric := BuildMetric(query) + require.Equal(t, "500ms-avg", metric["downsample"], "should convert 0.5s to 500ms") + }) + + t.Run("Metric with no downsampleInterval uses milliseconds for sub-second query interval", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 500 * time.Millisecond, + } + + metric := BuildMetric(query) + require.Equal(t, "500ms-avg", metric["downsample"], "should use query interval formatted as milliseconds") + }) + + t.Run("Metric with no downsampleInterval uses minutes for longer intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "sum", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 5 * time.Minute, + } + + metric := BuildMetric(query) + require.Equal(t, "5m-sum", metric["downsample"], "should use query interval formatted as minutes") + }) + + t.Run("Metric with no downsampleInterval uses hours for multi-hour intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "max", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 2 * time.Hour, + } + + metric := BuildMetric(query) + require.Equal(t, "2h-max", metric["downsample"], "should use query interval formatted as hours") + }) + + t.Run("Metric with no downsampleInterval uses days for multi-day intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "min", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 48 * time.Hour, + } + + metric := BuildMetric(query) + require.Equal(t, "2d-min", metric["downsample"], "should use query interval formatted as days") + }) + + t.Run("Build metric with explicitTags enabled", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": true, + "explicitTags": true, + "tags": { + "host": "server01" + } + }`, + ), + } + + metric := BuildMetric(query) + require.True(t, metric["explicitTags"].(bool), "explicitTags should be true") + + metricTags := metric["tags"].(map[string]any) + require.Equal(t, "server01", metricTags["host"]) + }) + + t.Run("Build metric with explicitTags disabled does not include explicitTags", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": true, + "explicitTags": false, + "tags": { + "host": "server01" + } + }`, + ), + } + + metric := BuildMetric(query) + require.Nil(t, metric["explicitTags"], "explicitTags should not be present when false") + }) +} + +func TestOpenTsdbExecutor(t *testing.T) { t.Run("create request", func(t *testing.T) { - req, err := service.createRequest(context.Background(), logger, &datasourceInfo{}, OpenTsdbQuery{}) + req, err := CreateRequest(context.Background(), logger, &datasourceInfo{}, OpenTsdbQuery{}) require.NoError(t, err) assert.Equal(t, "POST", req.Method) @@ -89,7 +243,7 @@ func TestOpenTsdbExecutor(t *testing.T) { response := `{ invalid }` tsdbVersion := float32(4) - result, err := service.parseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}, "A", tsdbVersion) + result, err := ParseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}, "A", tsdbVersion) require.Nil(t, result) require.Error(t, err) }) @@ -119,13 +273,14 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(4) resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + result, err := ParseResponse(logger, &resp, "A", tsdbVersion) require.NoError(t, err) frame := result.Responses["A"] @@ -160,13 +315,14 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(3) resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + result, err := ParseResponse(logger, &resp, "A", tsdbVersion) require.NoError(t, err) frame := result.Responses["A"] @@ -232,13 +388,14 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(3) resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + result, err := ParseResponse(logger, &resp, "A", tsdbVersion) require.NoError(t, err) frame := result.Responses["A"] @@ -275,6 +432,7 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = myRefid @@ -282,7 +440,7 @@ func TestOpenTsdbExecutor(t *testing.T) { resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, myRefid, tsdbVersion) + result, err := ParseResponse(logger, &resp, myRefid, tsdbVersion) require.NoError(t, err) if diff := cmp.Diff(testFrame, result.Responses[myRefid].Frames[0], data.FrameTestCompareOptions()...); diff != "" { @@ -290,6 +448,45 @@ func TestOpenTsdbExecutor(t *testing.T) { } }) + t.Run("tagKeys are returned sorted alphabetically in frame metadata", func(t *testing.T) { + response := ` + [ + { + "metric": "cpu.usage", + "dps": [ + [1405544146, 75.5] + ], + "tags" : { + "zone": "us-east-1", + "host": "server01", + "app": "api", + "env": "production" + } + } + ]` + + tsdbVersion := float32(4) + + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} + resp.StatusCode = 200 + result, err := ParseResponse(logger, &resp, "A", tsdbVersion) + require.NoError(t, err) + + frame := result.Responses["A"].Frames[0] + require.NotNil(t, frame.Meta, "frame metadata should not be nil") + require.NotNil(t, frame.Meta.Custom, "frame custom metadata should not be nil") + + customMeta, ok := frame.Meta.Custom.(map[string]any) + require.True(t, ok, "custom metadata should be a map") + + tagKeys, ok := customMeta["tagKeys"].([]string) + require.True(t, ok, "tagKeys should be present and be a string slice") + require.Len(t, tagKeys, 4, "should have 4 tag keys") + + expectedTagKeys := []string{"app", "env", "host", "zone"} + require.Equal(t, expectedTagKeys, tagKeys, "tagKeys should be sorted alphabetically") + }) + t.Run("Build metric with downsampling enabled", func(t *testing.T) { query := backend.DataQuery{ JSON: []byte(` @@ -304,7 +501,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) require.Len(t, metric, 3) require.Equal(t, "cpu.average.percent", metric["metric"]) @@ -326,7 +523,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) require.Len(t, metric, 2) require.Equal(t, "cpu.average.percent", metric["metric"]) @@ -347,7 +544,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) require.Len(t, metric, 3) require.Equal(t, "cpu.average.percent", metric["metric"]) @@ -373,7 +570,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) require.Len(t, metric, 3) require.Equal(t, "cpu.average.percent", metric["metric"]) @@ -404,7 +601,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) require.Len(t, metric, 5) require.Equal(t, "cpu.average.percent", metric["metric"]) @@ -439,7 +636,7 @@ func TestOpenTsdbExecutor(t *testing.T) { ), } - metric := service.buildMetric(query) + metric := BuildMetric(query) t.Log(metric) require.Len(t, metric, 5) diff --git a/pkg/tsdb/opentsdb/standalone/datasource.go b/pkg/tsdb/opentsdb/standalone/datasource.go index c2eacaf1d53..97241d651d6 100644 --- a/pkg/tsdb/opentsdb/standalone/datasource.go +++ b/pkg/tsdb/opentsdb/standalone/datasource.go @@ -10,8 +10,9 @@ import ( ) var ( - _ backend.QueryDataHandler = (*Datasource)(nil) - _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CallResourceHandler = (*Datasource)(nil) + _ backend.QueryDataHandler = (*Datasource)(nil) ) type Datasource struct { @@ -24,10 +25,14 @@ func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instanc }, nil } -func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return d.Service.QueryData(ctx, req) -} - func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { return d.Service.CheckHealth(ctx, req) } + +func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return d.Service.CallResource(ctx, req, sender) +} + +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/types.go b/pkg/tsdb/opentsdb/types.go index 19d2ba75197..89aed49baa8 100644 --- a/pkg/tsdb/opentsdb/types.go +++ b/pkg/tsdb/opentsdb/types.go @@ -7,8 +7,9 @@ type OpenTsdbQuery struct { } type OpenTsdbCommon struct { - Metric string `json:"metric"` - Tags map[string]string `json:"tags"` + Metric string `json:"metric"` + Tags map[string]string `json:"tags"` + AggregateTags []string `json:"aggregateTags"` } type OpenTsdbResponse struct { diff --git a/pkg/tsdb/opentsdb/utils.go b/pkg/tsdb/opentsdb/utils.go new file mode 100644 index 00000000000..ddfa8122fce --- /dev/null +++ b/pkg/tsdb/opentsdb/utils.go @@ -0,0 +1,304 @@ +package opentsdb + +import ( + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "sort" + "strconv" + "strings" + "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" +) + +func FormatDownsampleInterval(ms int64) string { + duration := time.Duration(ms) * time.Millisecond + + seconds := int64(duration / time.Second) + if seconds < 60 { + if seconds < 1 { + return strconv.FormatInt(ms, 10) + "ms" + } + return strconv.FormatInt(seconds, 10) + "s" + } + + minutes := int64(duration / time.Minute) + if minutes < 60 { + return strconv.FormatInt(minutes, 10) + "m" + } + + hours := int64(duration / time.Hour) + if hours < 24 { + return strconv.FormatInt(hours, 10) + "h" + } + + days := int64(duration / (24 * time.Hour)) + return strconv.FormatInt(days, 10) + "d" +} + +func BuildMetric(query backend.DataQuery) map[string]any { + metric := make(map[string]any) + + var model QueryModel + if err := json.Unmarshal(query.JSON, &model); err != nil { + return nil + } + + // Setting metric and aggregator + metric["metric"] = model.Metric + metric["aggregator"] = model.Aggregator + + // Setting downsampling options + if !model.DisableDownsampling { + downsampleInterval := model.DownsampleInterval + if downsampleInterval == "" { + if ms := query.Interval.Milliseconds(); ms > 0 { + downsampleInterval = FormatDownsampleInterval(ms) + } else { + downsampleInterval = "1m" + } + } else if strings.Contains(downsampleInterval, ".") && strings.HasSuffix(downsampleInterval, "s") { + if val, err := strconv.ParseFloat(strings.TrimSuffix(downsampleInterval, "s"), 64); err == nil { + downsampleInterval = strconv.FormatInt(int64(val*1000), 10) + "ms" + } + } + + downsample := downsampleInterval + "-" + model.DownsampleAggregator + if model.DownsampleFillPolicy != "" && model.DownsampleFillPolicy != "none" { + metric["downsample"] = downsample + "-" + model.DownsampleFillPolicy + } else { + metric["downsample"] = downsample + } + } + + // Setting rate options + if model.ShouldComputeRate { + metric["rate"] = true + rateOptions := make(map[string]any) + rateOptions["counter"] = model.IsCounter + + var counterMax *float64 + if model.CounterMax != "" { + if val, err := strconv.ParseFloat(model.CounterMax, 64); err == nil { + counterMax = &val + } + } + if counterMax != nil { + rateOptions["counterMax"] = *counterMax + } + + var counterResetValue *float64 + if model.CounterResetValue != "" { + if val, err := strconv.ParseFloat(model.CounterResetValue, 64); err == nil { + counterResetValue = &val + } + } + if counterResetValue != nil { + rateOptions["resetValue"] = *counterResetValue + } + + if counterMax == nil && (counterResetValue == nil || *counterResetValue == 0) { + rateOptions["dropResets"] = true + } + + metric["rateOptions"] = rateOptions + } + + // Setting tags + if len(model.Tags) > 0 { + metric["tags"] = model.Tags + } + + // Setting filters + if len(model.Filters) > 0 { + metric["filters"] = model.Filters + } + + if model.ExplicitTags { + metric["explicitTags"] = true + } + + return metric +} + +func CreateRequest(ctx context.Context, logger log.Logger, dsInfo *datasourceInfo, data OpenTsdbQuery) (*http.Request, error) { + u, err := url.Parse(dsInfo.URL) + if err != nil { + return nil, err + } + u.Path = path.Join(u.Path, "api/query") + if dsInfo.TSDBVersion == 4 { + queryParams := u.Query() + queryParams.Set("arrays", "true") + u.RawQuery = queryParams.Encode() + } + + postData, err := json.Marshal(data) + if err != nil { + logger.Info("Failed marshaling data", "error", err) + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(string(postData))) + if err != nil { + logger.Info("Failed to create request", "error", err) + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + return req, nil +} + +func DecodeResponseBody(res *http.Response, logger log.Logger) ([]byte, error) { + encoding := res.Header.Get("Content-Encoding") + var reader io.Reader + + switch encoding { + case "gzip": + gzipReader, err := gzip.NewReader(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { + if err := gzipReader.Close(); err != nil { + logger.Warn("Failed to close gzip reader", "error", err) + } + }() + reader = gzipReader + default: + reader = res.Body + } + + body, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + return body, nil +} + +func CreateDataFrame(val OpenTsdbCommon, length int, refID string) *data.Frame { + labels := data.Labels{} + for label, value := range val.Tags { + labels[label] = value + } + + tagKeys := make([]string, 0, len(val.Tags)+len(val.AggregateTags)) + for tagKey := range val.Tags { + tagKeys = append(tagKeys, tagKey) + } + sort.Strings(tagKeys) + tagKeys = append(tagKeys, val.AggregateTags...) + + frame := data.NewFrameOfFieldTypes(val.Metric, length, data.FieldTypeTime, data.FieldTypeFloat64) + frame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMulti, + TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": tagKeys}, + } + frame.RefID = refID + timeField := frame.Fields[0] + timeField.Name = data.TimeSeriesTimeFieldName + dataField := frame.Fields[1] + dataField.Name = val.Metric + dataField.Labels = labels + + return frame +} + +func ParseResponse(logger log.Logger, res *http.Response, refID string, tsdbVersion float32) (*backend.QueryDataResponse, error) { + resp := backend.NewQueryDataResponse() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer func() { + if err := res.Body.Close(); err != nil { + logger.Warn("Failed to close response body", "err", err) + } + }() + + if res.StatusCode/100 != 2 { + logger.Info("Request failed", "status", res.Status, "body", string(body)) + return nil, fmt.Errorf("request failed, status: %s", res.Status) + } + + frames := data.Frames{} + + var responseData []OpenTsdbResponse + var responseData24 []OpenTsdbResponse24 + if tsdbVersion == 4 { + err = json.Unmarshal(body, &responseData24) + if err != nil { + logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } + + frames = ParseResponse24(responseData24, refID, frames) + } else { + err = json.Unmarshal(body, &responseData) + if err != nil { + logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } + + frames, err = ParseResponseLT24(responseData, refID, frames) + if err != nil { + return nil, err + } + } + + result := resp.Responses[refID] + result.Frames = frames + resp.Responses[refID] = result + return resp, nil +} + +func ParseResponse24(responseData []OpenTsdbResponse24, refID string, frames data.Frames) data.Frames { + for _, val := range responseData { + frame := CreateDataFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) + + for i, point := range val.DataPoints { + frame.SetRow(i, time.Unix(int64(point[0]), 0).UTC(), point[1]) + } + + frames = append(frames, frame) + } + + return frames +} + +func ParseResponseLT24(responseData []OpenTsdbResponse, refID string, frames data.Frames) (data.Frames, error) { + for _, val := range responseData { + frame := CreateDataFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) + + // Order the timestamps in ascending order to avoid issues like https://github.com/grafana/grafana/issues/38729 + timestamps := make([]string, 0, len(val.DataPoints)) + for timestamp := range val.DataPoints { + timestamps = append(timestamps, timestamp) + } + sort.Strings(timestamps) + + for i, timeString := range timestamps { + timestamp, err := strconv.ParseInt(timeString, 10, 64) + if err != nil { + logger.Info("Failed to unmarshal opentsdb timestamp", "timestamp", timeString) + return frames, err + } + frame.SetRow(i, time.Unix(timestamp, 0).UTC(), val.DataPoints[timeString]) + } + + frames = append(frames, frame) + } + + return frames, nil +} diff --git a/pkg/tsdb/tempo/tempo.go b/pkg/tsdb/tempo/tempo.go index 1224c3b7029..cb797184a60 100644 --- a/pkg/tsdb/tempo/tempo.go +++ b/pkg/tsdb/tempo/tempo.go @@ -280,7 +280,15 @@ func (s *Service) handleTagValues(rw http.ResponseWriter, req *http.Request) { return } - tempoPath := fmt.Sprintf("api/v2/search/tag/%s/values", encodedTag) + // escape tag + tag, err := url.PathUnescape(encodedTag) + if err != nil { + s.logger.Error("Failed to unescape", "error", err, "tag", encodedTag) + http.Error(rw, "Invalid 'tag' parameter", http.StatusBadRequest) + return + } + + tempoPath := fmt.Sprintf("api/v2/search/tag/%s/values", tag) s.proxyToTempo(rw, req, tempoPath) } diff --git a/project.json b/project.json index a4a3fa81c09..702efba58af 100644 --- a/project.json +++ b/project.json @@ -39,7 +39,8 @@ "inputs": [ "{workspaceRoot}/scripts/cli/generateSassVariableFiles.ts", "{workspaceRoot}/packages/grafana-data/src/themes/**", - "{workspaceRoot}/packages/grafana-ui/src/themes/**" + "{workspaceRoot}/packages/grafana-ui/src/themes/**", + "{workspaceRoot}/package.json" ], "outputs": [ "{workspaceRoot}/public/sass/_variables.generated.scss", diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index d2fdd854168..9f09c0135d7 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -2263,8 +2263,7 @@ "operationId": "getTeamGroupsApi", "parameters": [ { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true @@ -2308,8 +2307,7 @@ } }, { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true @@ -2350,8 +2348,7 @@ "in": "query" }, { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true diff --git a/public/api-merged.json b/public/api-merged.json index 6effd7054fa..323a545e0cf 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -3402,11 +3402,12 @@ }, "/dashboards/home": { "get": { + "description": "NOTE: the home dashboard is configured in preferences. This API will be removed in G13", "tags": [ "dashboards" ], - "summary": "Get home dashboard.", "operationId": "getHomeDashboard", + "deprecated": true, "responses": { "200": { "$ref": "#/responses/getHomeDashboardResponse" @@ -9926,8 +9927,7 @@ "operationId": "getTeamGroupsApi", "parameters": [ { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true @@ -9971,8 +9971,7 @@ } }, { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true @@ -10013,8 +10012,7 @@ "in": "query" }, { - "type": "integer", - "format": "int64", + "type": "string", "name": "teamId", "in": "path", "required": true diff --git a/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts b/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts index c3c44ede267..0d39d4fa402 100644 --- a/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts +++ b/public/app/api/clients/provisioning/utils/createOnCacheEntryAdded.ts @@ -58,6 +58,7 @@ export function createOnCacheEntryAdded(resourceName: string) { }); } catch (error) { console.error('Error in onCacheEntryAdded:', error); + return; } await cacheEntryRemoved; diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index f3ab1b63c1b..e340e8276b8 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -119,7 +119,7 @@ export class AppChromeService { }; private getUpdatedHistory(newState: AppChromeState): HistoryEntry[] { - const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }, true); + const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }); const newPageNav = newState.pageNav || newState.sectionNav.node; let entries = store.getObject(HISTORY_LOCAL_STORAGE_KEY, []); diff --git a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx b/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx deleted file mode 100644 index ca660e8750c..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { css } from '@emotion/css'; -import type { JSX } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; - -export interface Props { - children: JSX.Element; -} - -export const FeatureHighlight = ({ children }: Props): JSX.Element => { - const styles = useStyles2(getStyles); - return ( - <> - {children} - - - ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - highlight: css({ - backgroundColor: theme.colors.success.main, - borderRadius: theme.shape.radius.circle, - width: '6px', - height: '6px', - display: 'inline-block;', - position: 'absolute', - top: '50%', - transform: 'translateY(-50%)', - }), - }; -}; diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index dead4bd810e..f4ec7598fa2 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -62,10 +62,6 @@ export const MegaMenu = memo( const activeItem = getActiveItem(navItems, state.sectionNav.node, location.pathname); - const handleMegaMenu = () => { - chrome.setMegaMenuOpen(!state.megaMenuOpen); - }; - const handleDockedMenu = () => { chrome.setMegaMenuDocked(!state.megaMenuDocked); if (state.megaMenuDocked) { @@ -108,7 +104,7 @@ export const MegaMenu = memo( return (
- +
{ - if (onToggleSort) { + if (onToggleSort && isSortable) { onToggleSort(columnTitle); } }} @@ -119,7 +119,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ table: css({ width: '100%', 'th:first-child': { - width: '100%', borderBottom: `1px solid ${theme.colors.border.weak}`, }, }), diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx new file mode 100644 index 00000000000..4ca95aa395c --- /dev/null +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx @@ -0,0 +1,112 @@ +import { render, screen } from '@testing-library/react'; + +import { LegendTableItem } from './VizLegendTableItem'; +import { VizLegendItem } from './types'; + +describe('LegendTableItem', () => { + const mockItem: VizLegendItem = { + label: 'Series 1', + color: 'red', + yAxis: 1, + }; + + it('renders without crashing', () => { + const { container } = render( + + + + +
+ ); + expect(container.querySelector('tr')).toBeInTheDocument(); + }); + + it('renders label text', () => { + render( + + + + +
+ ); + expect(screen.getByText('Series 1')).toBeInTheDocument(); + }); + + it('renders with long label text', () => { + const longLabelItem: VizLegendItem = { + ...mockItem, + label: 'This is a very long series name that should be scrollable in the table cell', + }; + render( + + + + +
+ ); + expect( + screen.getByText('This is a very long series name that should be scrollable in the table cell') + ).toBeInTheDocument(); + }); + + it('renders stat values when provided', () => { + const itemWithStats: VizLegendItem = { + ...mockItem, + getDisplayValues: () => [ + { numeric: 100, text: '100', title: 'Max' }, + { numeric: 50, text: '50', title: 'Min' }, + ], + }; + render( + + + + +
+ ); + expect(screen.getByText('100')).toBeInTheDocument(); + expect(screen.getByText('50')).toBeInTheDocument(); + }); + + it('renders right y-axis indicator when yAxis is 2', () => { + const rightAxisItem: VizLegendItem = { + ...mockItem, + yAxis: 2, + }; + render( + + + + +
+ ); + expect(screen.getByText('(right y-axis)')).toBeInTheDocument(); + }); + + it('calls onLabelClick when label is clicked', () => { + const onLabelClick = jest.fn(); + render( + + + + +
+ ); + const button = screen.getByRole('button'); + button.click(); + expect(onLabelClick).toHaveBeenCalledWith(mockItem, expect.any(Object)); + }); + + it('does not call onClick when readonly', () => { + const onLabelClick = jest.fn(); + render( + + + + +
+ ); + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + }); +}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx index 335cf4309e9..56ec6cb733e 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx @@ -69,7 +69,7 @@ export const LegendTableItem = ({ return (
+ - +
+ +